import {HarnessLoader, parallel} from '@angular/cdk/testing';
import {TestbedHarnessEnvironment} from '@angular/cdk/testing/testbed';
import {Component} from '@angular/core';
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {MatNativeDateModule} from '@angular/material/core';
import {
  DateRange,
  DefaultMatCalendarRangeStrategy,
  MAT_DATE_RANGE_SELECTION_STRATEGY,
  MatDatepickerModule,
} from '@angular/material/datepicker';
import {CalendarView, MatCalendarHarness} from './calendar-harness';

/** Date at which the calendars are set. */
const calendarDate = new Date(2020, 7, 1);

describe('MatCalendarHarness', () => {
  let fixture: ComponentFixture<CalendarHarnessTest>;
  let loader: HarnessLoader;

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [MatNativeDateModule, MatDatepickerModule, CalendarHarnessTest],
      providers: [
        {
          // Usually it's the date range picker that provides the default range selection strategy,
          // but since we're testing the calendar on its own, we have to provide it manually.
          provide: MAT_DATE_RANGE_SELECTION_STRATEGY,
          useClass: DefaultMatCalendarRangeStrategy,
        },
      ],
    });

    fixture = TestBed.createComponent(CalendarHarnessTest);
    fixture.detectChanges();
    loader = TestbedHarnessEnvironment.loader(fixture);
  });

  it('should load all calendar harnesses', async () => {
    const calendars = await loader.getAllHarnesses(MatCalendarHarness);
    expect(calendars.length).toBe(2);
  });

  it('should go to a different view', async () => {
    const calendar = await loader.getHarness(MatCalendarHarness.with({selector: '#single'}));
    expect(await calendar.getCurrentView()).toBe(CalendarView.MONTH);

    await calendar.changeView();
    expect(await calendar.getCurrentView()).toBe(CalendarView.MULTI_YEAR);
  });

  it('should get the current view label', async () => {
    const calendar = await loader.getHarness(MatCalendarHarness.with({selector: '#single'}));
    expect(await calendar.getCurrentViewLabel()).toBe('AUG 2020');

    await calendar.changeView();
    expect(await calendar.getCurrentViewLabel()).toBe('2016 – 2039');
  });

  it('should go to the next page in the view', async () => {
    const calendar = await loader.getHarness(MatCalendarHarness.with({selector: '#single'}));
    expect(await calendar.getCurrentViewLabel()).toBe('AUG 2020');

    await calendar.next();
    expect(await calendar.getCurrentViewLabel()).toBe('SEP 2020');
  });

  it('should go to the previous page in the view', async () => {
    const calendar = await loader.getHarness(MatCalendarHarness.with({selector: '#single'}));
    expect(await calendar.getCurrentViewLabel()).toBe('AUG 2020');

    await calendar.previous();
    expect(await calendar.getCurrentViewLabel()).toBe('JUL 2020');
  });

  it('should get all of the date cells inside the calendar', async () => {
    const calendar = await loader.getHarness(MatCalendarHarness.with({selector: '#single'}));
    expect((await calendar.getCells()).length).toBe(31);
  });

  it('should get the text of a calendar cell', async () => {
    const calendar = await loader.getHarness(MatCalendarHarness.with({selector: '#single'}));
    const cells = await calendar.getCells();

    expect(await cells[0].getText()).toBe('1');
    expect(await cells[15].getText()).toBe('16');
    expect(await cells[30].getText()).toBe('31');
  });

  it('should be able to select a specific cell through the calendar', async () => {
    const calendar = await loader.getHarness(MatCalendarHarness.with({selector: '#single'}));
    const targetCell = (await calendar.getCells({text: '16'}))[0];
    expect(await targetCell.isSelected()).toBe(false);

    await calendar.selectCell({text: '16'});
    expect(await targetCell.isSelected()).toBe(true);
  });

  it('should get the aria-label of a cell', async () => {
    const calendar = await loader.getHarness(MatCalendarHarness.with({selector: '#single'}));
    const cells = await calendar.getCells();

    expect(await cells[0].getAriaLabel()).toBe('August 1, 2020');
    expect(await cells[15].getAriaLabel()).toBe('August 16, 2020');
    expect(await cells[30].getAriaLabel()).toBe('August 31, 2020');
  });

  it('should get the disabled state of a cell', async () => {
    fixture.componentInstance.minDate = new Date(
      calendarDate.getFullYear(),
      calendarDate.getMonth(),
      20,
    );
    fixture.changeDetectorRef.markForCheck();

    const calendar = await loader.getHarness(MatCalendarHarness.with({selector: '#single'}));
    const cells = await calendar.getCells();

    expect(await cells[0].isDisabled()).toBe(true);
    expect(await cells[15].isDisabled()).toBe(true);
    expect(await cells[30].isDisabled()).toBe(false);
  });

  it('should select a cell', async () => {
    const calendar = await loader.getHarness(MatCalendarHarness.with({selector: '#single'}));
    const cell = (await calendar.getCells())[10];
    expect(await cell.isSelected()).toBe(false);

    await cell.select();
    expect(await cell.isSelected()).toBe(true);
  });

  it('should get whether a cell is active', async () => {
    const calendar = await loader.getHarness(MatCalendarHarness.with({selector: '#single'}));
    const cells = await calendar.getCells();

    expect(await cells[0].isActive()).toBe(true);
    expect(await cells[15].isActive()).toBe(false);
  });

  it('should get the state of the cell within the main range', async () => {
    const calendar = await loader.getHarness(MatCalendarHarness.with({selector: '#range'}));
    const allCells = await calendar.getCells();
    const [initialStartStates, initialInRangeStates, initialEndStates] = await parallel(() => [
      parallel(() => allCells.map(cell => cell.isRangeStart())),
      parallel(() => allCells.map(cell => cell.isInRange())),
      parallel(() => allCells.map(cell => cell.isRangeEnd())),
    ]);

    expect(initialStartStates.every(state => state === false)).toBe(true);
    expect(initialInRangeStates.every(state => state === false)).toBe(true);
    expect(initialEndStates.every(state => state === false)).toBe(true);

    await (await calendar.getCells({text: '5'}))[0].select();
    await (await calendar.getCells({text: '8'}))[0].select();

    expect(await allCells[4].isRangeStart()).toBe(true);
    expect(await allCells[4].isInRange()).toBe(true);
    expect(await allCells[4].isRangeEnd()).toBe(false);

    expect(await allCells[5].isRangeStart()).toBe(false);
    expect(await allCells[5].isInRange()).toBe(true);
    expect(await allCells[5].isRangeEnd()).toBe(false);

    expect(await allCells[6].isRangeStart()).toBe(false);
    expect(await allCells[6].isInRange()).toBe(true);
    expect(await allCells[6].isRangeEnd()).toBe(false);

    expect(await allCells[7].isRangeStart()).toBe(false);
    expect(await allCells[7].isInRange()).toBe(true);
    expect(await allCells[7].isRangeEnd()).toBe(true);
  });

  it('should get the state of the cell within the comparison range', async () => {
    const calendar = await loader.getHarness(MatCalendarHarness.with({selector: '#range'}));
    const allCells = await calendar.getCells();
    const [initialStartStates, initialInRangeStates, initialEndStates] = await parallel(() => [
      parallel(() => allCells.map(cell => cell.isComparisonRangeStart())),
      parallel(() => allCells.map(cell => cell.isInComparisonRange())),
      parallel(() => allCells.map(cell => cell.isComparisonRangeEnd())),
    ]);

    expect(initialStartStates.every(state => state === false)).toBe(true);
    expect(initialInRangeStates.every(state => state === false)).toBe(true);
    expect(initialEndStates.every(state => state === false)).toBe(true);

    fixture.componentInstance.comparisonStart = new Date(
      calendarDate.getFullYear(),
      calendarDate.getMonth(),
      5,
    );
    fixture.componentInstance.comparisonEnd = new Date(
      calendarDate.getFullYear(),
      calendarDate.getMonth(),
      8,
    );
    fixture.changeDetectorRef.markForCheck();

    expect(await allCells[4].isComparisonRangeStart()).toBe(true);
    expect(await allCells[4].isInComparisonRange()).toBe(true);
    expect(await allCells[4].isComparisonRangeEnd()).toBe(false);

    expect(await allCells[5].isComparisonRangeStart()).toBe(false);
    expect(await allCells[5].isInComparisonRange()).toBe(true);
    expect(await allCells[5].isComparisonRangeEnd()).toBe(false);

    expect(await allCells[6].isComparisonRangeStart()).toBe(false);
    expect(await allCells[6].isInComparisonRange()).toBe(true);
    expect(await allCells[6].isComparisonRangeEnd()).toBe(false);

    expect(await allCells[7].isComparisonRangeStart()).toBe(false);
    expect(await allCells[7].isInComparisonRange()).toBe(true);
    expect(await allCells[7].isComparisonRangeEnd()).toBe(true);
  });

  it('should get the state of the cell within the preview range', async () => {
    const calendar = await loader.getHarness(MatCalendarHarness.with({selector: '#range'}));
    const allCells = await calendar.getCells();
    const [initialStartStates, initialInRangeStates, initialEndStates] = await parallel(() => [
      parallel(() => allCells.map(cell => cell.isPreviewRangeStart())),
      parallel(() => allCells.map(cell => cell.isInPreviewRange())),
      parallel(() => allCells.map(cell => cell.isPreviewRangeEnd())),
    ]);

    expect(initialStartStates.every(state => state === false)).toBe(true);
    expect(initialInRangeStates.every(state => state === false)).toBe(true);
    expect(initialEndStates.every(state => state === false)).toBe(true);

    await (await calendar.getCells({text: '5'}))[0].select();
    await (await calendar.getCells({text: '8'}))[0].hover();

    expect(await allCells[4].isPreviewRangeStart()).toBe(true);
    expect(await allCells[4].isInPreviewRange()).toBe(true);
    expect(await allCells[4].isPreviewRangeEnd()).toBe(false);

    expect(await allCells[5].isPreviewRangeStart()).toBe(false);
    expect(await allCells[5].isInPreviewRange()).toBe(true);
    expect(await allCells[5].isPreviewRangeEnd()).toBe(false);

    expect(await allCells[6].isPreviewRangeStart()).toBe(false);
    expect(await allCells[6].isInPreviewRange()).toBe(true);
    expect(await allCells[6].isPreviewRangeEnd()).toBe(false);

    expect(await allCells[7].isPreviewRangeStart()).toBe(false);
    expect(await allCells[7].isInPreviewRange()).toBe(true);
    expect(await allCells[7].isPreviewRangeEnd()).toBe(true);
  });

  it('should filter cells by their text', async () => {
    const calendar = await loader.getHarness(MatCalendarHarness.with({selector: '#single'}));
    const cells = await calendar.getCells({text: /^3/});
    expect(await parallel(() => cells.map(cell => cell.getText()))).toEqual(['3', '30', '31']);
  });

  it('should filter cells by their selected state', async () => {
    const calendar = await loader.getHarness(MatCalendarHarness.with({selector: '#single'}));
    const allCells = await calendar.getCells();

    await allCells[0].select();

    const selectedCells = await calendar.getCells({selected: true});
    expect(await parallel(() => selectedCells.map(cell => cell.getText()))).toEqual(['1']);
  });

  it('should filter cells by their active state', async () => {
    const calendar = await loader.getHarness(MatCalendarHarness.with({selector: '#single'}));
    const cells = await calendar.getCells({active: true});
    expect(await parallel(() => cells.map(cell => cell.getText()))).toEqual(['1']);
  });

  it('should filter cells by their disabled state', async () => {
    fixture.componentInstance.minDate = new Date(
      calendarDate.getFullYear(),
      calendarDate.getMonth(),
      3,
    );
    fixture.changeDetectorRef.markForCheck();

    const calendar = await loader.getHarness(MatCalendarHarness.with({selector: '#single'}));
    const cells = await calendar.getCells({disabled: true});
    expect(await parallel(() => cells.map(cell => cell.getText()))).toEqual(['1', '2']);
  });

  it('should filter cells based on whether they are inside the comparison range', async () => {
    const calendar = await loader.getHarness(MatCalendarHarness.with({selector: '#range'}));

    fixture.componentInstance.comparisonStart = new Date(
      calendarDate.getFullYear(),
      calendarDate.getMonth(),
      5,
    );
    fixture.componentInstance.comparisonEnd = new Date(
      calendarDate.getFullYear(),
      calendarDate.getMonth(),
      8,
    );
    fixture.changeDetectorRef.markForCheck();

    const cells = await calendar.getCells({inComparisonRange: true});
    expect(await parallel(() => cells.map(cell => cell.getText()))).toEqual(['5', '6', '7', '8']);
  });

  it('should filter cells based on whether they are inside the preview range', async () => {
    const calendar = await loader.getHarness(MatCalendarHarness.with({selector: '#range'}));

    await (await calendar.getCells({text: '5'}))[0].select();
    await (await calendar.getCells({text: '8'}))[0].hover();

    const cells = await calendar.getCells({inPreviewRange: true});
    expect(await parallel(() => cells.map(cell => cell.getText()))).toEqual(['5', '6', '7', '8']);
  });
});

@Component({
  template: `
    <mat-calendar
      id="single"
      [startAt]="startAt"
      [minDate]="minDate"
      [selected]="singleValue"
      (selectedChange)="singleValue = $event"></mat-calendar>

    <mat-calendar
      id="range"
      [startAt]="startAt"
      [minDate]="minDate"
      [selected]="rangeValue"
      [comparisonStart]="comparisonStart"
      [comparisonEnd]="comparisonEnd"
      (selectedChange)="rangeChanged($event)"></mat-calendar>
  `,
  standalone: true,
  imports: [MatNativeDateModule, MatDatepickerModule],
})
class CalendarHarnessTest {
  // Start the datepickers off at a specific date so tests
  // run consistently no matter what the current date is.
  readonly startAt = new Date(calendarDate);
  minDate: Date | null;
  singleValue: Date | null = null;
  rangeValue = new DateRange<Date>(null, null);
  comparisonStart: Date | null = null;
  comparisonEnd: Date | null = null;

  rangeChanged(selectedDate: Date) {
    let {start, end} = this.rangeValue;

    if (start == null || end != null) {
      start = selectedDate;
    } else if (end == null) {
      end = selectedDate;
    }

    this.rangeValue = new DateRange<Date>(start, end);
  }
}
