import {
  dispatchFakeEvent,
  dispatchMouseEvent,
  dispatchTouchEvent,
} from '@angular/cdk/testing/private';
import {Component} from '@angular/core';
import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing';
import {By} from '@angular/platform-browser';
import {MatCalendarBody, MatCalendarCell, MatCalendarUserEvent} from './calendar-body';

describe('MatCalendarBody', () => {
  beforeEach(waitForAsync(() => {
    TestBed.configureTestingModule({
      imports: [MatCalendarBody, StandardCalendarBody, RangeCalendarBody],
    });
  }));

  describe('standard calendar body', () => {
    let fixture: ComponentFixture<StandardCalendarBody>;
    let testComponent: StandardCalendarBody;
    let calendarBodyNativeElement: Element;
    let rowEls: Element[];
    let labelEls: Element[];
    let cellEls: Element[];

    function refreshElementLists() {
      rowEls = Array.from(calendarBodyNativeElement.querySelectorAll('tr'));
      labelEls = Array.from(calendarBodyNativeElement.querySelectorAll('.mat-calendar-body-label'));
      cellEls = Array.from(calendarBodyNativeElement.querySelectorAll('.mat-calendar-body-cell'));
    }

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

      const calendarBodyDebugElement = fixture.debugElement.query(By.directive(MatCalendarBody))!;
      calendarBodyNativeElement = calendarBodyDebugElement.nativeElement;
      testComponent = fixture.componentInstance;

      refreshElementLists();
    });

    it('creates body', () => {
      expect(rowEls.length).toBe(3);
      expect(labelEls.length).toBe(1);
      expect(cellEls.length).toBe(14);
    });

    it('highlights today', () => {
      const todayCells = calendarBodyNativeElement.querySelectorAll('.mat-calendar-body-today')!;
      expect(todayCells.length).toBe(1);

      const todayCell = todayCells[0];

      expect(todayCell).not.toBeNull();
      expect(todayCell.textContent!.trim()).toBe('3');
    });

    it('sets aria-current="date" on today', () => {
      const todayCells = calendarBodyNativeElement.querySelectorAll(
        '[aria-current="date"] .mat-calendar-body-today',
      )!;
      expect(todayCells.length).toBe(1);

      const todayCell = todayCells[0];

      expect(todayCell).not.toBeNull();
      expect(todayCell.textContent!.trim()).toBe('3');
    });

    it('does not highlight today if today is not within the scope', () => {
      testComponent.todayValue = 100000;
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();

      const todayCell = calendarBodyNativeElement.querySelector('.mat-calendar-body-today')!;
      expect(todayCell).toBeNull();
    });

    it('does not set aria-current="date" on any cell if today is not ' + 'the scope', () => {
      testComponent.todayValue = 100000;
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();

      const todayCell = calendarBodyNativeElement.querySelector(
        '[aria-current="date"] .mat-calendar-body-today',
      )!;
      expect(todayCell).toBeNull();
    });

    it('highlights selected', () => {
      const selectedCell = calendarBodyNativeElement.querySelector('.mat-calendar-body-selected')!;
      expect(selectedCell).not.toBeNull();
      expect(selectedCell.innerHTML.trim()).toBe('4');
    });

    it('should set aria-pressed correctly', () => {
      const pressedCells = cellEls.filter(c => c.getAttribute('aria-pressed') === 'true');
      const depressedCells = cellEls.filter(c => c.getAttribute('aria-pressed') === 'false');

      expect(pressedCells.length).withContext('Expected one cell to be marked as pressed.').toBe(1);
      expect(depressedCells.length)
        .withContext('Expected remaining cells to be marked as not pressed.')
        .toBe(cellEls.length - 1);
    });

    it('places label in first row if space is available', () => {
      testComponent.rows[0] = testComponent.rows[0].slice(3);
      testComponent.rows = testComponent.rows.slice();
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();
      refreshElementLists();

      expect(rowEls.length).toBe(2);
      expect(labelEls.length).toBe(1);
      expect(cellEls.length).toBe(11);
      expect(rowEls[0].firstElementChild!.classList)
        .withContext('first cell should be the label')
        .toContain('mat-calendar-body-label');
      expect(labelEls[0].getAttribute('colspan')).toBe('3');
    });

    it('cell should be selected on click', () => {
      const todayElement = calendarBodyNativeElement.querySelector(
        '.mat-calendar-body-today',
      ) as HTMLElement;
      todayElement.click();
      fixture.detectChanges();

      expect(todayElement.classList)
        .withContext('today should be selected')
        .toContain('mat-calendar-body-selected');
    });

    it('should mark active date', () => {
      expect((cellEls[10] as HTMLElement).innerText.trim()).toBe('11');
      expect(cellEls[10].classList).toContain('mat-calendar-body-active');
    });

    it('should set a class on even dates', () => {
      expect((cellEls[0] as HTMLElement).innerText.trim()).toBe('1');
      expect((cellEls[1] as HTMLElement).innerText.trim()).toBe('2');
      expect(cellEls[0].classList).not.toContain('even');
      expect(cellEls[1].classList).toContain('even');
    });

    it('should have a focus indicator', () => {
      expect(cellEls.every(element => !!element.querySelector('.mat-focus-indicator'))).toBe(true);
    });
  });

  describe('range calendar body', () => {
    const startClass = 'mat-calendar-body-range-start';
    const inRangeClass = 'mat-calendar-body-in-range';
    const endClass = 'mat-calendar-body-range-end';
    const comparisonStartClass = 'mat-calendar-body-comparison-start';
    const inComparisonClass = 'mat-calendar-body-in-comparison-range';
    const comparisonEndClass = 'mat-calendar-body-comparison-end';
    const bridgeStart = 'mat-calendar-body-comparison-bridge-start';
    const bridgeEnd = 'mat-calendar-body-comparison-bridge-end';
    const previewStartClass = 'mat-calendar-body-preview-start';
    const inPreviewClass = 'mat-calendar-body-in-preview';
    const previewEndClass = 'mat-calendar-body-preview-end';
    let fixture: ComponentFixture<RangeCalendarBody>;
    let testComponent: RangeCalendarBody;
    let cells: HTMLElement[];

    beforeEach(() => {
      fixture = TestBed.createComponent(RangeCalendarBody);
      fixture.detectChanges();
      testComponent = fixture.componentInstance;
      cells = Array.from(fixture.nativeElement.querySelectorAll('.mat-calendar-body-cell'));
    });

    it('should render a range', () => {
      testComponent.startValue = 1;
      testComponent.endValue = 5;
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();

      expect(cells[0].classList).toContain(startClass);
      expect(cells[1].classList).toContain(inRangeClass);
      expect(cells[2].classList).toContain(inRangeClass);
      expect(cells[3].classList).toContain(inRangeClass);
      expect(cells[4].classList).toContain(endClass);
    });

    it('should render a comparison range', () => {
      testComponent.comparisonStart = 1;
      testComponent.comparisonEnd = 5;
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();

      expect(cells[0].classList).toContain(comparisonStartClass);
      expect(cells[1].classList).toContain(inComparisonClass);
      expect(cells[2].classList).toContain(inComparisonClass);
      expect(cells[3].classList).toContain(inComparisonClass);
      expect(cells[4].classList).toContain(comparisonEndClass);
    });

    it('should be able to render two completely overlapping ranges', () => {
      testComponent.startValue = testComponent.comparisonStart = 1;
      testComponent.endValue = testComponent.comparisonEnd = 5;
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();

      expect(cells[0].classList).toContain(startClass);
      expect(cells[0].classList).toContain(comparisonStartClass);

      expect(cells[1].classList).toContain(inRangeClass);
      expect(cells[1].classList).toContain(inComparisonClass);

      expect(cells[2].classList).toContain(inRangeClass);
      expect(cells[2].classList).toContain(inComparisonClass);

      expect(cells[3].classList).toContain(inRangeClass);
      expect(cells[3].classList).toContain(inComparisonClass);

      expect(cells[4].classList).toContain(endClass);
      expect(cells[4].classList).toContain(comparisonEndClass);
    });

    it(
      'should mark a cell as a start bridge if it is the end of the main range ' +
        'and the start of the comparison',
      () => {
        testComponent.startValue = 1;
        testComponent.endValue = 5;
        testComponent.comparisonStart = 5;
        testComponent.comparisonEnd = 10;
        fixture.changeDetectorRef.markForCheck();
        fixture.detectChanges();

        expect(cells[4].classList).toContain(bridgeStart);
      },
    );

    it('should not mark a cell as a start bridge if there is no end range value', () => {
      testComponent.startValue = 1;
      testComponent.endValue = null;
      testComponent.comparisonStart = 5;
      testComponent.comparisonEnd = 10;
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();

      expect(cells.some(cell => cell.classList.contains(bridgeStart))).toBe(false);
    });

    it(
      'should mark a cell as an end bridge if it is the start of the main range ' +
        'and the end of the comparison',
      () => {
        testComponent.comparisonStart = 1;
        testComponent.comparisonEnd = 5;
        testComponent.startValue = 5;
        testComponent.endValue = 10;
        fixture.changeDetectorRef.markForCheck();
        fixture.detectChanges();

        expect(cells[4].classList).toContain(bridgeEnd);
      },
    );

    it('should not mark a cell as an end bridge if there is no end range value', () => {
      testComponent.comparisonStart = 1;
      testComponent.comparisonEnd = 5;
      testComponent.startValue = 5;
      testComponent.endValue = null;
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();

      expect(cells.some(cell => cell.classList.contains(bridgeEnd))).toBe(false);
    });

    it('should be able to show a main range inside a comparison range', () => {
      testComponent.comparisonStart = 1;
      testComponent.comparisonEnd = 5;
      testComponent.startValue = 2;
      testComponent.endValue = 4;
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();

      expect(cells[0].classList).toContain(comparisonStartClass);

      expect(cells[1].classList).toContain(inComparisonClass);
      expect(cells[1].classList).toContain(startClass);

      expect(cells[2].classList).toContain(inComparisonClass);
      expect(cells[2].classList).toContain(inRangeClass);

      expect(cells[3].classList).toContain(inComparisonClass);
      expect(cells[3].classList).toContain(endClass);

      expect(cells[4].classList).toContain(comparisonEndClass);
    });

    it('should be able to show a comparison range inside a main range', () => {
      testComponent.startValue = 1;
      testComponent.endValue = 5;
      testComponent.comparisonStart = 2;
      testComponent.comparisonEnd = 4;
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();

      expect(cells[0].classList).toContain(startClass);

      expect(cells[1].classList).toContain(inRangeClass);
      expect(cells[1].classList).toContain(comparisonStartClass);

      expect(cells[2].classList).toContain(inRangeClass);
      expect(cells[2].classList).toContain(inComparisonClass);

      expect(cells[3].classList).toContain(inRangeClass);
      expect(cells[3].classList).toContain(comparisonEndClass);

      expect(cells[4].classList).toContain(endClass);
    });

    it('should be able to show a range that is larger than the calendar', () => {
      testComponent.startValue = -10;
      testComponent.endValue = 100;
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();

      expect(cells.every(cell => cell.classList.contains(inRangeClass))).toBe(true);
      expect(cells.some(cell => cell.classList.contains(startClass))).toBe(false);
      expect(cells.some(cell => cell.classList.contains(endClass))).toBe(false);
    });

    it('should be able to show a comparison range that is larger than the calendar', () => {
      testComponent.comparisonStart = -10;
      testComponent.comparisonEnd = 100;
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();

      expect(cells.every(cell => cell.classList.contains(inComparisonClass))).toBe(true);
      expect(cells.some(cell => cell.classList.contains(comparisonStartClass))).toBe(false);
      expect(cells.some(cell => cell.classList.contains(comparisonEndClass))).toBe(false);
    });

    it('should be able to show a range that starts before the beginning of the calendar', () => {
      testComponent.startValue = -10;
      testComponent.endValue = 2;
      fixture.changeDetectorRef.markForCheck();

      fixture.detectChanges();

      expect(cells.some(cell => cell.classList.contains(startClass))).toBe(false);
      expect(cells[0].classList).toContain(inRangeClass);
      expect(cells[1].classList).toContain(endClass);
    });

    it('should be able to show a comparison range that starts before the beginning of the calendar', () => {
      testComponent.comparisonStart = -10;
      testComponent.comparisonEnd = 2;
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();

      expect(cells.some(cell => cell.classList.contains(comparisonStartClass))).toBe(false);
      expect(cells[0].classList).toContain(inComparisonClass);
      expect(cells[1].classList).toContain(comparisonEndClass);
    });

    it('should be able to show a range that ends after the end of the calendar', () => {
      testComponent.startValue = 27;
      testComponent.endValue = 50;
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();

      expect(cells.some(cell => cell.classList.contains(endClass))).toBe(false);
      expect(cells[26].classList).toContain(startClass);
      expect(cells[27].classList).toContain(inRangeClass);
    });

    it('should be able to show a comparison range that ends after the end of the calendar', () => {
      testComponent.comparisonStart = 27;
      testComponent.comparisonEnd = 50;
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();

      expect(cells.some(cell => cell.classList.contains(comparisonEndClass))).toBe(false);
      expect(cells[26].classList).toContain(comparisonStartClass);
      expect(cells[27].classList).toContain(inComparisonClass);
    });

    it('should be able to show a range that ends after the end of the calendar', () => {
      testComponent.startValue = 27;
      testComponent.endValue = 50;
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();

      expect(cells.some(cell => cell.classList.contains(endClass))).toBe(false);
      expect(cells[26].classList).toContain(startClass);
      expect(cells[27].classList).toContain(inRangeClass);
    });

    it('should not to mark a date as both the start and end', () => {
      testComponent.startValue = 1;
      testComponent.endValue = 1;
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();

      expect(cells[0].classList).not.toContain(startClass);
      expect(cells[0].classList).not.toContain(inRangeClass);
      expect(cells[0].classList).not.toContain(endClass);
    });

    it('should not mark a date as both the comparison start and end', () => {
      testComponent.comparisonStart = 1;
      testComponent.comparisonEnd = 1;
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();

      expect(cells[0].classList).not.toContain(comparisonStartClass);
      expect(cells[0].classList).not.toContain(inComparisonClass);
      expect(cells[0].classList).not.toContain(comparisonEndClass);
    });

    it('should not mark a date as the range end if it comes before the start', () => {
      testComponent.startValue = 2;
      testComponent.endValue = 1;
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();

      expect(cells[0].classList).not.toContain(endClass);
      expect(cells[0].classList).not.toContain(inRangeClass);
      expect(cells[1].classList).not.toContain(startClass);
    });

    it('should not mark a date as the comparison range end if it comes before the start', () => {
      testComponent.comparisonStart = 2;
      testComponent.comparisonEnd = 1;
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();

      expect(cells[0].classList).not.toContain(comparisonEndClass);
      expect(cells[0].classList).not.toContain(inComparisonClass);
      expect(cells[1].classList).not.toContain(comparisonStartClass);
    });

    it('should not show a range if there is no start', () => {
      testComponent.startValue = null;
      testComponent.endValue = 10;
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();

      expect(cells.some(cell => cell.classList.contains(inRangeClass))).toBe(false);
      expect(cells.some(cell => cell.classList.contains(endClass))).toBe(false);
    });

    it('should not show a comparison range if there is no start', () => {
      testComponent.comparisonStart = null;
      testComponent.comparisonEnd = 10;
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();

      expect(cells.some(cell => cell.classList.contains(inComparisonClass))).toBe(false);
      expect(cells.some(cell => cell.classList.contains(comparisonEndClass))).toBe(false);
    });

    it('should not show a comparison range if there is no end', () => {
      testComponent.comparisonStart = 10;
      testComponent.comparisonEnd = null;
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();

      expect(cells.some(cell => cell.classList.contains(inComparisonClass))).toBe(false);
      expect(cells.some(cell => cell.classList.contains(comparisonEndClass))).toBe(false);
    });

    it('should preview the selected range after the user clicks on a start and hovers away', () => {
      cells[2].click();
      fixture.detectChanges();

      dispatchMouseEvent(cells[5], 'mouseenter');
      fixture.detectChanges();

      expect(cells[2].classList).toContain(previewStartClass);
      expect(cells[3].classList).toContain(inPreviewClass);
      expect(cells[4].classList).toContain(inPreviewClass);
      expect(cells[5].classList).toContain(previewEndClass);

      // Go a few cells ahead.
      dispatchMouseEvent(cells[7], 'mouseenter');
      fixture.detectChanges();

      expect(cells[5].classList).not.toContain(previewEndClass);
      expect(cells[5].classList).toContain(inPreviewClass);
      expect(cells[6].classList).toContain(inPreviewClass);
      expect(cells[7].classList).toContain(previewEndClass);

      // Go back a few cells.
      dispatchMouseEvent(cells[4], 'mouseenter');
      fixture.detectChanges();

      expect(cells[5].classList).not.toContain(inPreviewClass);
      expect(cells[6].classList).not.toContain(inPreviewClass);
      expect(cells[7].classList).not.toContain(previewEndClass);
      expect(cells[3].classList).toContain(inPreviewClass);
      expect(cells[4].classList).toContain(previewEndClass);
    });

    it('should preview the selected range after the user selects a start and moves focus away', () => {
      cells[2].click();
      fixture.detectChanges();

      dispatchFakeEvent(cells[5], 'focus');
      fixture.detectChanges();

      expect(cells[2].classList).toContain(previewStartClass);
      expect(cells[3].classList).toContain(inPreviewClass);
      expect(cells[4].classList).toContain(inPreviewClass);
      expect(cells[5].classList).toContain(previewEndClass);

      // Go a few cells ahead.
      dispatchFakeEvent(cells[7], 'focus');
      fixture.detectChanges();

      expect(cells[5].classList).not.toContain(previewEndClass);
      expect(cells[5].classList).toContain(inPreviewClass);
      expect(cells[6].classList).toContain(inPreviewClass);
      expect(cells[7].classList).toContain(previewEndClass);

      // Go back a few cells.
      dispatchFakeEvent(cells[4], 'focus');
      fixture.detectChanges();

      expect(cells[5].classList).not.toContain(inPreviewClass);
      expect(cells[6].classList).not.toContain(inPreviewClass);
      expect(cells[7].classList).not.toContain(previewEndClass);
      expect(cells[3].classList).toContain(inPreviewClass);
      expect(cells[4].classList).toContain(previewEndClass);
    });

    it('should not be able to extend the range before the start', () => {
      cells[5].click();
      fixture.detectChanges();

      dispatchMouseEvent(cells[2], 'mouseenter');
      fixture.detectChanges();

      expect(cells[5].classList).not.toContain(startClass);
      expect(cells[5].classList).not.toContain(previewStartClass);
      expect(cells.some(cell => cell.classList.contains(inPreviewClass))).toBe(false);
    });

    it(
      'should be able to show a range, starting before the beginning of the calendar, ' +
        'while hovering',
      () => {
        fixture.componentInstance.startValue = -1;
        fixture.changeDetectorRef.markForCheck();
        fixture.detectChanges();

        dispatchMouseEvent(cells[2], 'mouseenter');
        fixture.detectChanges();

        expect(cells.some(cell => cell.classList.contains(previewStartClass))).toBe(false);
        expect(cells[0].classList).toContain(inPreviewClass);
        expect(cells[1].classList).toContain(inPreviewClass);
        expect(cells[2].classList).toContain(previewEndClass);
      },
    );

    it(
      'should be able to show a range, starting before the beginning of the calendar, ' +
        'while moving focus',
      () => {
        fixture.componentInstance.startValue = -1;
        fixture.changeDetectorRef.markForCheck();
        fixture.detectChanges();

        dispatchMouseEvent(cells[2], 'focus');
        fixture.detectChanges();

        expect(cells.some(cell => cell.classList.contains(previewStartClass))).toBe(false);
        expect(cells[0].classList).toContain(inPreviewClass);
        expect(cells[1].classList).toContain(inPreviewClass);
        expect(cells[2].classList).toContain(previewEndClass);
      },
    );

    it('should remove the preview if the user moves their pointer away', () => {
      cells[2].click();
      fixture.detectChanges();

      dispatchMouseEvent(cells[4], 'mouseenter');
      fixture.detectChanges();

      expect(cells[2].classList).toContain(previewStartClass);
      expect(cells[3].classList).toContain(inPreviewClass);
      expect(cells[4].classList).toContain(previewEndClass);

      // Move the pointer away.
      dispatchMouseEvent(cells[4], 'mouseleave');
      fixture.detectChanges();

      expect(cells[2].classList).not.toContain(previewStartClass);
      expect(cells[3].classList).not.toContain(inPreviewClass);
      expect(cells[4].classList).not.toContain(previewEndClass);

      // Move the pointer back in to a different cell.
      dispatchMouseEvent(cells[5], 'mouseenter');
      fixture.detectChanges();

      expect(cells[2].classList).toContain(previewStartClass);
      expect(cells[3].classList).toContain(inPreviewClass);
      expect(cells[4].classList).toContain(inPreviewClass);
      expect(cells[5].classList).toContain(previewEndClass);
    });

    it('should remove the preview if the user moves their focus away', () => {
      cells[2].click();
      fixture.detectChanges();

      dispatchFakeEvent(cells[4], 'focus');
      fixture.detectChanges();

      expect(cells[2].classList).toContain(previewStartClass);
      expect(cells[3].classList).toContain(inPreviewClass);
      expect(cells[4].classList).toContain(previewEndClass);

      // Move the pointer away.
      dispatchFakeEvent(cells[4], 'blur');
      fixture.detectChanges();

      expect(cells[2].classList).not.toContain(previewStartClass);
      expect(cells[3].classList).not.toContain(inPreviewClass);
      expect(cells[4].classList).not.toContain(previewEndClass);

      // Move the pointer back in to a different cell.
      dispatchFakeEvent(cells[5], 'focus');
      fixture.detectChanges();

      expect(cells[2].classList).toContain(previewStartClass);
      expect(cells[3].classList).toContain(inPreviewClass);
      expect(cells[4].classList).toContain(inPreviewClass);
      expect(cells[5].classList).toContain(previewEndClass);
    });

    it('should mark a cell as being identical to the comparison range', () => {
      testComponent.comparisonStart = testComponent.comparisonEnd = 3;
      fixture.changeDetectorRef.markForCheck();
      fixture.detectChanges();

      const comparisonIdenticalCells: NodeListOf<HTMLElement> =
        fixture.nativeElement.querySelectorAll('.mat-calendar-body-comparison-identical');

      expect(comparisonIdenticalCells.length).toBe(1);
      expect(cells[2].contains(comparisonIdenticalCells[0])).toBe(true);
      expect(
        cells.some(cell => {
          const classList = cell.classList;
          return (
            classList.contains(startClass) ||
            classList.contains(inRangeClass) ||
            classList.contains(endClass) ||
            classList.contains(comparisonStartClass) ||
            classList.contains(inComparisonClass) ||
            classList.contains(comparisonEndClass)
          );
        }),
      ).toBe(false);
    });

    describe('drag and drop ranges', () => {
      beforeEach(() => {
        // Pre-select a range to drag.
        fixture.componentInstance.startValue = 4;
        fixture.componentInstance.endValue = 6;
        fixture.changeDetectorRef.markForCheck();
        fixture.detectChanges();
      });

      it('triggers and previews a drag (mouse)', () => {
        dispatchMouseEvent(cells[3], 'mousedown');
        fixture.detectChanges();
        expect(fixture.componentInstance.drag).not.toBe(null);

        // Expand to earlier.
        dispatchMouseEvent(cells[2], 'mouseenter');
        fixture.detectChanges();
        expect(cells[2].classList).toContain(previewStartClass);
        expect(cells[3].classList).toContain(inPreviewClass);
        expect(cells[4].classList).toContain(inPreviewClass);
        expect(cells[5].classList).toContain(previewEndClass);

        // End drag.
        dispatchMouseEvent(cells[2], 'mouseup');
        expect(fixture.componentInstance.drag).toBe(null);
      });

      it('triggers and previews a drag (touch)', () => {
        dispatchTouchEvent(cells[3], 'touchstart');
        fixture.detectChanges();
        expect(fixture.componentInstance.drag).not.toBe(null);

        // Expand to earlier.
        const rect = cells[2].getBoundingClientRect();

        dispatchTouchEvent(cells[2], 'touchmove', rect.left, rect.top, rect.left, rect.top);
        fixture.detectChanges();
        expect(cells[2].classList).toContain(previewStartClass);
        expect(cells[3].classList).toContain(inPreviewClass);
        expect(cells[4].classList).toContain(inPreviewClass);
        expect(cells[5].classList).toContain(previewEndClass);

        // End drag.
        dispatchTouchEvent(cells[2], 'touchend', rect.left, rect.top, rect.left, rect.top);
        expect(fixture.componentInstance.drag).toBe(null);
      });
    });
  });
});

@Component({
  template: `
    <table mat-calendar-body
          [label]="label"
          [rows]="rows"
          [todayValue]="todayValue"
          [startValue]="selectedValue"
          [endValue]="selectedValue"
          [labelMinRequiredCells]="labelMinRequiredCells"
          [numCols]="numCols"
          [activeCell]="10"
          (selectedValueChange)="onSelect($event)">
    </table>`,
  standalone: true,
  imports: [MatCalendarBody],
})
class StandardCalendarBody {
  label = 'Jan 2017';
  rows = createCalendarCells(2);
  todayValue = 3;
  selectedValue = 4;
  labelMinRequiredCells = 3;
  numCols = 7;

  onSelect(event: MatCalendarUserEvent<number>) {
    this.selectedValue = event.value;
  }
}

@Component({
  template: `
    <table mat-calendar-body
          [isRange]="true"
          [rows]="rows"
          [startValue]="startValue"
          [endValue]="endValue"
          [comparisonStart]="comparisonStart"
          [comparisonEnd]="comparisonEnd"
          [previewStart]="previewStart"
          [previewEnd]="previewEnd"
          (selectedValueChange)="onSelect($event)"
          (previewChange)="previewChanged($event)"
          (dragStarted)="dragStarted($event)"
          (dragEnded)="dragEnded($event)"
          >
    </table>`,
  standalone: true,
  imports: [MatCalendarBody],
})
class RangeCalendarBody {
  rows = createCalendarCells(4);
  startValue: number | null;
  endValue: number | null;
  comparisonStart: number | null;
  comparisonEnd: number | null;
  previewStart: number | null;
  previewEnd: number | null;
  drag: MatCalendarUserEvent<unknown> | null = null;

  onSelect(event: MatCalendarUserEvent<number>) {
    const value = event.value;
    if (!this.startValue) {
      this.startValue = value;
    } else if (!this.endValue) {
      this.endValue = value;
    } else {
      this.startValue = value;
      this.endValue = null;
    }
  }

  previewChanged(event: MatCalendarUserEvent<MatCalendarCell<Date> | null>) {
    this.previewStart = this.startValue;
    this.previewEnd = event.value?.compareValue || null;

    if (this.drag) {
      // For sake of testing, hardcode a preview for drags.
      this.previewStart = this.startValue! - 1;
      this.previewEnd = this.endValue;
    }
  }

  dragStarted(event: MatCalendarUserEvent<unknown>) {
    this.drag = event;
  }

  dragEnded(event: MatCalendarUserEvent<unknown>) {
    this.drag = null;
  }
}

/**
 * Creates a 2d array of days, split into weeks.
 * @param weeks Number of weeks that should be generated.
 */
function createCalendarCells(weeks: number): MatCalendarCell[][] {
  const rows: number[][] = [];
  let dayCounter = 1;

  for (let i = 0; i < weeks; i++) {
    const row = [];

    while (row.length < 7) {
      row.push(dayCounter++);
    }

    rows.push(row);
  }

  return rows.map(row =>
    row.map(cell => {
      return new MatCalendarCell(
        cell,
        `${cell}`,
        `${cell}-label`,
        true,
        cell % 2 === 0 ? 'even' : undefined,
        cell,
        cell,
      );
    }),
  );
}
