// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart';

import 'feedback_tester.dart';

void main() {
  TestWidgetsFlutterBinding.ensureInitialized();

  final Finder nextMonthIcon = find.byWidgetPredicate((Widget w) => w is IconButton && (w.tooltip?.startsWith('Next month') ?? false));
  final Finder previousMonthIcon = find.byWidgetPredicate((Widget w) => w is IconButton && (w.tooltip?.startsWith('Previous month') ?? false));

  Widget calendarDatePicker({
    Key? key,
    DateTime? initialDate,
    DateTime? firstDate,
    DateTime? lastDate,
    DateTime? currentDate,
    ValueChanged<DateTime>? onDateChanged,
    ValueChanged<DateTime>? onDisplayedMonthChanged,
    DatePickerMode initialCalendarMode = DatePickerMode.day,
    SelectableDayPredicate? selectableDayPredicate,
    TextDirection textDirection = TextDirection.ltr,
    bool? useMaterial3,
  }) {
    return MaterialApp(
      theme: ThemeData(useMaterial3: useMaterial3),
      home: Material(
        child: Directionality(
          textDirection: textDirection,
          child: CalendarDatePicker(
            key: key,
            initialDate: initialDate,
            firstDate: firstDate ?? DateTime(2001),
            lastDate: lastDate ?? DateTime(2031, DateTime.december, 31),
            currentDate: currentDate ?? DateTime(2016, DateTime.january, 3),
            onDateChanged: onDateChanged ?? (DateTime date) {},
            onDisplayedMonthChanged: onDisplayedMonthChanged,
            initialCalendarMode: initialCalendarMode,
            selectableDayPredicate: selectableDayPredicate,
          ),
        ),
      ),
    );
  }

  Widget yearPicker({
    Key? key,
    DateTime? selectedDate,
    DateTime? initialDate,
    DateTime? firstDate,
    DateTime? lastDate,
    DateTime? currentDate,
    ValueChanged<DateTime>? onChanged,
    TextDirection textDirection = TextDirection.ltr,
  }) {
    return MaterialApp(
      home: Material(
        child: Directionality(
          textDirection: textDirection,
          child: YearPicker(
            key: key,
            selectedDate: selectedDate ?? DateTime(2016, DateTime.january, 15),
            firstDate: firstDate ?? DateTime(2001),
            lastDate: lastDate ?? DateTime(2031, DateTime.december, 31),
            currentDate: currentDate ?? DateTime(2016, DateTime.january, 3),
            onChanged: onChanged ?? (DateTime date) {},
          ),
        ),
      ),
    );
  }

  group('CalendarDatePicker', () {
    testWidgetsWithLeakTracking('Can select a day', (WidgetTester tester) async {
      DateTime? selectedDate;
      await tester.pumpWidget(calendarDatePicker(
        initialDate: DateTime(2016, DateTime.january, 15),
        onDateChanged: (DateTime date) => selectedDate = date,
      ));
      await tester.tap(find.text('12'));
      expect(selectedDate, equals(DateTime(2016, DateTime.january, 12)));
    });

    testWidgetsWithLeakTracking('Can select a day with nothing first selected', (WidgetTester tester) async {
      DateTime? selectedDate;
      await tester.pumpWidget(calendarDatePicker(
        onDateChanged: (DateTime date) => selectedDate = date,
      ));
      await tester.tap(find.text('12'));
      expect(selectedDate, equals(DateTime(2016, DateTime.january, 12)));
    });

    testWidgetsWithLeakTracking('Can select a month', (WidgetTester tester) async {
      DateTime? displayedMonth;
      await tester.pumpWidget(calendarDatePicker(
        initialDate: DateTime(2016, DateTime.january, 15),
        onDisplayedMonthChanged: (DateTime date) => displayedMonth = date,
      ));
      expect(find.text('January 2016'), findsOneWidget);

      // Go back two months
      await tester.tap(previousMonthIcon);
      await tester.pumpAndSettle();
      expect(find.text('December 2015'), findsOneWidget);
      expect(displayedMonth, equals(DateTime(2015, DateTime.december)));
      await tester.tap(previousMonthIcon);
      await tester.pumpAndSettle();
      expect(find.text('November 2015'), findsOneWidget);
      expect(displayedMonth, equals(DateTime(2015, DateTime.november)));

      // Go forward a month
      await tester.tap(nextMonthIcon);
      await tester.pumpAndSettle();
      expect(find.text('December 2015'), findsOneWidget);
      expect(displayedMonth, equals(DateTime(2015, DateTime.december)));
    });

    testWidgetsWithLeakTracking('Can select a month with nothing first selected', (WidgetTester tester) async {
      DateTime? displayedMonth;
      await tester.pumpWidget(calendarDatePicker(
        onDisplayedMonthChanged: (DateTime date) => displayedMonth = date,
      ));
      expect(find.text('January 2016'), findsOneWidget);

      // Go back two months
      await tester.tap(previousMonthIcon);
      await tester.pumpAndSettle();
      expect(find.text('December 2015'), findsOneWidget);
      expect(displayedMonth, equals(DateTime(2015, DateTime.december)));
      await tester.tap(previousMonthIcon);
      await tester.pumpAndSettle();
      expect(find.text('November 2015'), findsOneWidget);
      expect(displayedMonth, equals(DateTime(2015, DateTime.november)));

      // Go forward a month
      await tester.tap(nextMonthIcon);
      await tester.pumpAndSettle();
      expect(find.text('December 2015'), findsOneWidget);
      expect(displayedMonth, equals(DateTime(2015, DateTime.december)));
    });

    testWidgetsWithLeakTracking('Can select a year', (WidgetTester tester) async {
      DateTime? displayedMonth;
      await tester.pumpWidget(calendarDatePicker(
        initialDate: DateTime(2016, DateTime.january, 15),
        onDisplayedMonthChanged: (DateTime date) => displayedMonth = date,
      ));

      await tester.tap(find.text('January 2016')); // Switch to year mode.
      await tester.pumpAndSettle();
      await tester.tap(find.text('2018'));
      await tester.pumpAndSettle();
      expect(find.text('January 2018'), findsOneWidget);
      expect(displayedMonth, equals(DateTime(2018)));
    });

    testWidgetsWithLeakTracking('Can select a year with nothing first selected', (WidgetTester tester) async {
      DateTime? displayedMonth;
      await tester.pumpWidget(calendarDatePicker(
        onDisplayedMonthChanged: (DateTime date) => displayedMonth = date,
      ));

      await tester.tap(find.text('January 2016')); // Switch to year mode.
      await tester.pumpAndSettle();
      await tester.tap(find.text('2018'));
      await tester.pumpAndSettle();
      expect(find.text('January 2018'), findsOneWidget);
      expect(displayedMonth, equals(DateTime(2018)));
    });

    testWidgetsWithLeakTracking('Selecting date does not change displayed month', (WidgetTester tester) async {
      DateTime? selectedDate;
      DateTime? displayedMonth;
      await tester.pumpWidget(calendarDatePicker(
        initialDate: DateTime(2020, DateTime.march, 15),
        onDateChanged: (DateTime date) => selectedDate = date,
        onDisplayedMonthChanged: (DateTime date) => displayedMonth = date,
      ));

      await tester.tap(nextMonthIcon);
      await tester.pumpAndSettle();
      expect(find.text('April 2020'), findsOneWidget);
      expect(displayedMonth, equals(DateTime(2020, DateTime.april)));

      await tester.tap(find.text('25'));
      await tester.pumpAndSettle();
      expect(find.text('April 2020'), findsOneWidget);
      expect(displayedMonth, equals(DateTime(2020, DateTime.april)));
      expect(selectedDate, equals(DateTime(2020, DateTime.april, 25)));
      // There isn't a 31 in April so there shouldn't be one if it is showing April.
      expect(find.text('31'), findsNothing);
    });

    testWidgetsWithLeakTracking('Changing year does change selected date', (WidgetTester tester) async {
      DateTime? selectedDate;
      await tester.pumpWidget(calendarDatePicker(
        initialDate: DateTime(2016, DateTime.january, 15),
        onDateChanged: (DateTime date) => selectedDate = date,
      ));
      await tester.tap(find.text('4'));
      expect(selectedDate, equals(DateTime(2016, DateTime.january, 4)));
      await tester.tap(find.text('January 2016'));
      await tester.pumpAndSettle();
      await tester.tap(find.text('2018'));
      await tester.pumpAndSettle();
      expect(selectedDate, equals(DateTime(2018, DateTime.january, 4)));
    });

    testWidgetsWithLeakTracking('Changing year for february 29th', (WidgetTester tester) async {
      DateTime? selectedDate;
      await tester.pumpWidget(calendarDatePicker(
        initialDate: DateTime(2020, DateTime.february, 29),
        onDateChanged: (DateTime date) => selectedDate = date,
      ));
      await tester.tap(find.text('February 2020'));
      await tester.pumpAndSettle();
      await tester.tap(find.text('2018'));
      await tester.pumpAndSettle();
      expect(selectedDate, equals(DateTime(2018, DateTime.february, 28)));
      await tester.tap(find.text('February 2018'));
      await tester.pumpAndSettle();
      await tester.tap(find.text('2020'));
      await tester.pumpAndSettle();
      // Changing back to 2020 the 29th is not selected anymore.
      expect(selectedDate, equals(DateTime(2020, DateTime.february, 28)));
    });

    testWidgetsWithLeakTracking('Changing year does not change the month', (WidgetTester tester) async {
      DateTime? displayedMonth;
      await tester.pumpWidget(calendarDatePicker(
        initialDate: DateTime(2016, DateTime.january, 15),
        onDisplayedMonthChanged: (DateTime date) => displayedMonth = date,
      ));
      await tester.tap(nextMonthIcon);
      await tester.pumpAndSettle();
      await tester.tap(nextMonthIcon);
      await tester.pumpAndSettle();
      await tester.tap(find.text('March 2016'));
      await tester.pumpAndSettle();
      await tester.tap(find.text('2018'));
      await tester.pumpAndSettle();
      expect(find.text('March 2018'), findsOneWidget);
      expect(displayedMonth, equals(DateTime(2018, DateTime.march)));
    });

    testWidgetsWithLeakTracking('Can select a year and then a day', (WidgetTester tester) async {
      DateTime? selectedDate;
      await tester.pumpWidget(calendarDatePicker(
        initialDate: DateTime(2016, DateTime.january, 15),
        onDateChanged: (DateTime date) => selectedDate = date,
      ));
      await tester.tap(find.text('January 2016')); // Switch to year mode.
      await tester.pumpAndSettle();
      await tester.tap(find.text('2017'));
      await tester.pumpAndSettle();
      await tester.tap(find.text('19'));
      expect(selectedDate, equals(DateTime(2017, DateTime.january, 19)));
    });

    testWidgetsWithLeakTracking('Cannot select a day outside bounds', (WidgetTester tester) async {
      final DateTime validDate = DateTime(2017, DateTime.january, 15);
      DateTime? selectedDate;
      await tester.pumpWidget(calendarDatePicker(
        initialDate: validDate,
        firstDate: validDate,
        lastDate: validDate,
        onDateChanged: (DateTime date) => selectedDate = date,
      ));

      // Earlier than firstDate. Should be ignored.
      await tester.tap(find.text('10'));
      expect(selectedDate, isNull);

      // Later than lastDate. Should be ignored.
      await tester.tap(find.text('20'));
      expect(selectedDate, isNull);

      // This one is just right.
      await tester.tap(find.text('15'));
      expect(selectedDate, validDate);
    });

    testWidgetsWithLeakTracking('Cannot navigate to a month outside bounds', (WidgetTester tester) async {
      DateTime? displayedMonth;
      await tester.pumpWidget(calendarDatePicker(
        firstDate: DateTime(2016, DateTime.december, 15),
        initialDate: DateTime(2017, DateTime.january, 15),
        lastDate: DateTime(2017, DateTime.february, 15),
        onDisplayedMonthChanged: (DateTime date) => displayedMonth = date,
      ));

      await tester.tap(nextMonthIcon);
      await tester.pumpAndSettle();
      expect(displayedMonth, equals(DateTime(2017, DateTime.february)));
      // Shouldn't be possible to keep going forward into March.
      expect(nextMonthIcon, findsNothing);

      await tester.tap(previousMonthIcon);
      await tester.pumpAndSettle();
      await tester.tap(previousMonthIcon);
      await tester.pumpAndSettle();
      expect(displayedMonth, equals(DateTime(2016, DateTime.december)));
      // Shouldn't be possible to keep going backward into November.
      expect(previousMonthIcon, findsNothing);
    });

    testWidgetsWithLeakTracking('Cannot select disabled year', (WidgetTester tester) async {
      DateTime? displayedMonth;
      await tester.pumpWidget(calendarDatePicker(
        firstDate: DateTime(2018, DateTime.june, 9),
        initialDate: DateTime(2018, DateTime.july, 4),
        lastDate: DateTime(2018, DateTime.december, 15),
        onDisplayedMonthChanged: (DateTime date) => displayedMonth = date,
      ));
      await tester.tap(find.text('July 2018')); // Switch to year mode.
      await tester.pumpAndSettle();
      await tester.tap(find.text('2016')); // Disabled, doesn't change the year.
      await tester.pumpAndSettle();
      await tester.tap(find.text('2020')); // Disabled, doesn't change the year.
      await tester.pumpAndSettle();

      await tester.tap(find.text('2018'));
      await tester.pumpAndSettle();
      // Nothing should have changed.
      expect(displayedMonth, isNull);
    });

    testWidgetsWithLeakTracking('Selecting firstDate year respects firstDate', (WidgetTester tester) async {
      DateTime? selectedDate;
      DateTime? displayedMonth;
      await tester.pumpWidget(calendarDatePicker(
        firstDate: DateTime(2016, DateTime.june, 9),
        initialDate: DateTime(2018, DateTime.may, 4),
        lastDate: DateTime(2019, DateTime.january, 15),
        onDateChanged: (DateTime date) => selectedDate = date,
        onDisplayedMonthChanged: (DateTime date) => displayedMonth = date,
      ));
      await tester.tap(find.text('May 2018'));
      await tester.pumpAndSettle();
      await tester.tap(find.text('2016'));
      await tester.pumpAndSettle();
      // Month should be clamped to June as the range starts at June 2016.
      expect(find.text('June 2016'), findsOneWidget);
      expect(displayedMonth, DateTime(2016, DateTime.june));
      expect(selectedDate, DateTime(2016, DateTime.june, 9));
    });

    testWidgetsWithLeakTracking('Selecting lastDate year respects lastDate', (WidgetTester tester) async {
      DateTime? selectedDate;
      DateTime? displayedMonth;
      await tester.pumpWidget(calendarDatePicker(
        firstDate: DateTime(2016, DateTime.june, 9),
        initialDate: DateTime(2018, DateTime.may, 4),
        lastDate: DateTime(2019, DateTime.january, 15),
        onDateChanged: (DateTime date) => selectedDate = date,
        onDisplayedMonthChanged: (DateTime date) => displayedMonth = date,
      ));
      // Selected date is now 2018-05-04 (initialDate).
      await tester.tap(find.text('May 2018'));
      // Selected date is still 2018-05-04.
      await tester.pumpAndSettle();
      await tester.tap(find.text('2019'));
      // Selected date would become 2019-05-04 but gets clamped to the month of lastDate, so 2019-01-04.
      await tester.pumpAndSettle();
      expect(find.text('January 2019'), findsOneWidget);
      expect(displayedMonth, DateTime(2019));
      expect(selectedDate, DateTime(2019, DateTime.january, 4));
    });

    testWidgetsWithLeakTracking('Selecting lastDate year respects lastDate', (WidgetTester tester) async {
      DateTime? selectedDate;
      DateTime? displayedMonth;
      await tester.pumpWidget(calendarDatePicker(
        firstDate: DateTime(2016, DateTime.june, 9),
        initialDate: DateTime(2018, DateTime.may, 15),
        lastDate: DateTime(2019, DateTime.january, 4),
        onDateChanged: (DateTime date) => selectedDate = date,
        onDisplayedMonthChanged: (DateTime date) => displayedMonth = date,
      ));
      // Selected date is now 2018-05-15 (initialDate).
      await tester.tap(find.text('May 2018'));
      // Selected date is still 2018-05-15.
      await tester.pumpAndSettle();
      await tester.tap(find.text('2019'));
      // Selected date would become 2019-05-15 but gets clamped to the month of lastDate, so 2019-01-15.
      // Day is now beyond the lastDate so that also gets clamped, to 2019-01-04.
      await tester.pumpAndSettle();
      expect(find.text('January 2019'), findsOneWidget);
      expect(displayedMonth, DateTime(2019));
      expect(selectedDate, DateTime(2019, DateTime.january, 4));
    });

    testWidgetsWithLeakTracking('Only predicate days are selectable', (WidgetTester tester) async {
      DateTime? selectedDate;
      await tester.pumpWidget(calendarDatePicker(
        firstDate: DateTime(2017, DateTime.january, 10),
        initialDate: DateTime(2017, DateTime.january, 16),
        lastDate: DateTime(2017, DateTime.january, 20),
        onDateChanged: (DateTime date) => selectedDate = date,
        selectableDayPredicate: (DateTime date) => date.day.isEven,
      ));
      await tester.tap(find.text('13')); // Odd, doesn't work.
      expect(selectedDate, isNull);
      await tester.tap(find.text('10')); // Even, works.
      expect(selectedDate, DateTime(2017, DateTime.january, 10));
      await tester.tap(find.text('17')); // Odd, doesn't work.
      expect(selectedDate, DateTime(2017, DateTime.january, 10));
    });

    testWidgetsWithLeakTracking('Can select initial calendar picker mode', (WidgetTester tester) async {
      await tester.pumpWidget(calendarDatePicker(
        initialDate: DateTime(2014, DateTime.january, 15),
        initialCalendarMode: DatePickerMode.year,
      ));
      // 2018 wouldn't be available if the year picker wasn't showing.
      // The initial current year is 2014.
      await tester.tap(find.text('2018'));
      await tester.pumpAndSettle();
      expect(find.text('January 2018'), findsOneWidget);
    });

    testWidgetsWithLeakTracking('Material2 - currentDate is highlighted', (WidgetTester tester) async {
      await tester.pumpWidget(calendarDatePicker(
        useMaterial3: false,
        initialDate: DateTime(2016, DateTime.january, 15),
        currentDate: DateTime(2016, 1, 2),
      ));
      const Color todayColor = Color(0xff2196f3); // default primary color
      expect(
        Material.of(tester.element(find.text('2'))),
        // The current day should be painted with a circle outline.
        paints..circle(
          color: todayColor,
          style: PaintingStyle.stroke,
          strokeWidth: 1.0,
        ),
      );
    });

    testWidgetsWithLeakTracking('Material3 - currentDate is highlighted', (WidgetTester tester) async {
      await tester.pumpWidget(calendarDatePicker(
        useMaterial3: true,
        initialDate: DateTime(2016, DateTime.january, 15),
        currentDate: DateTime(2016, 1, 2),
      ));
      const Color todayColor = Color(0xff6750a4); // default primary color
      expect(
        Material.of(tester.element(find.text('2'))),
        // The current day should be painted with a circle outline.
        paints..circle(
          color: todayColor,
          style: PaintingStyle.stroke,
          strokeWidth: 1.0,
        ),
      );
    });

    testWidgetsWithLeakTracking('Material2 - currentDate is highlighted even if it is disabled', (WidgetTester tester) async {
      await tester.pumpWidget(calendarDatePicker(
        useMaterial3: false,
        firstDate: DateTime(2016, 1, 3),
        lastDate: DateTime(2016, 1, 31),
        currentDate: DateTime(2016, 1, 2), // not between first and last
        initialDate: DateTime(2016, 1, 5),
      ));
      const Color disabledColor = Color(0x61000000); // default disabled color
      expect(
        Material.of(tester.element(find.text('2'))),
        // The current day should be painted with a circle outline.
        paints
          ..circle(
            color: disabledColor,
            style: PaintingStyle.stroke,
            strokeWidth: 1.0,
          ),
      );
    });

    testWidgetsWithLeakTracking('Material3 - currentDate is highlighted even if it is disabled', (WidgetTester tester) async {
      await tester.pumpWidget(calendarDatePicker(
        useMaterial3: true,
        firstDate: DateTime(2016, 1, 3),
        lastDate: DateTime(2016, 1, 31),
        currentDate: DateTime(2016, 1, 2), // not between first and last
        initialDate: DateTime(2016, 1, 5),
      ));
      const Color disabledColor = Color(0x616750a4); // default disabled color
      expect(
        Material.of(tester.element(find.text('2'))),
        // The current day should be painted with a circle outline.
        paints
          ..circle(
            color: disabledColor,
            style: PaintingStyle.stroke,
            strokeWidth: 1.0,
          ),
      );
    });

    testWidgetsWithLeakTracking('Selecting date does not switch picker to year selection', (WidgetTester tester) async {
      await tester.pumpWidget(calendarDatePicker(
        initialDate: DateTime(2020, DateTime.may, 10),
        initialCalendarMode: DatePickerMode.year,
      ));
      await tester.tap(find.text('2017'));
      await tester.pumpAndSettle();
      expect(find.text('May 2017'), findsOneWidget);
      await tester.tap(find.text('10'));
      await tester.pumpAndSettle();
      expect(find.text('May 2017'), findsOneWidget);
      expect(find.text('2017'), findsNothing);
    });

    testWidgetsWithLeakTracking('Selecting disabled date does not change current selection', (WidgetTester tester) async {
      DateTime day(int day) => DateTime(2020, DateTime.may, day);

      DateTime selection = day(2);
      await tester.pumpWidget(calendarDatePicker(
        initialDate: selection,
        firstDate: day(2),
        lastDate: day(3),
        onDateChanged: (DateTime date) {
          selection = date;
        },
      ));

      await tester.tap(find.text('3'));
      await tester.pumpAndSettle();
      expect(selection, day(3));
      await tester.tap(find.text('4'));
      await tester.pumpAndSettle();
      expect(selection, day(3));
      await tester.tap(find.text('5'));
      await tester.pumpAndSettle();
      expect(selection, day(3));
    });

    for (final bool useMaterial3 in <bool>[false, true]) {
      testWidgetsWithLeakTracking('Updates to initialDate parameter are not reflected in the state (useMaterial3=$useMaterial3)', (WidgetTester tester) async {
        final Key pickerKey = UniqueKey();
        final DateTime initialDate = DateTime(2020, 1, 21);
        final DateTime updatedDate = DateTime(1976, 2, 23);
        final DateTime firstDate = DateTime(1970);
        final DateTime lastDate = DateTime(2099, 31, 12);
        final Color selectedColor = useMaterial3 ? const Color(0xff6750a4) : const Color(0xff2196f3); // default primary color

        await tester.pumpWidget(calendarDatePicker(
          key: pickerKey,
          useMaterial3: useMaterial3,
          initialDate: initialDate,
          firstDate: firstDate,
          lastDate: lastDate,
          onDateChanged: (DateTime value) {},
        ));
        await tester.pumpAndSettle();

        // Month should show as January 2020.
        expect(find.text('January 2020'), findsOneWidget);
        // Selected date should be painted with a colored circle.
        expect(
          Material.of(tester.element(find.text('21'))),
          paints..circle(color: selectedColor, style: PaintingStyle.fill),
        );

        // Change to the updated initialDate.
        // This should have no effect, the initialDate is only the _initial_ date.
        await tester.pumpWidget(calendarDatePicker(
          key: pickerKey,
          useMaterial3: useMaterial3,
          initialDate: updatedDate,
          firstDate: firstDate,
          lastDate: lastDate,
          onDateChanged: (DateTime value) {},
        ));
        // Wait for the page scroll animation to finish.
        await tester.pumpAndSettle(const Duration(milliseconds: 200));

        // Month should show as January 2020 still.
        expect(find.text('January 2020'), findsOneWidget);
        expect(find.text('February 1976'), findsNothing);
        // Selected date should be painted with a colored circle.
        expect(
          Material.of(tester.element(find.text('21'))),
          paints..circle(color: selectedColor, style: PaintingStyle.fill),
        );
      });
    }

    testWidgetsWithLeakTracking('Updates to initialCalendarMode parameter is not reflected in the state', (WidgetTester tester) async {
      final Key pickerKey = UniqueKey();

      await tester.pumpWidget(calendarDatePicker(
        key: pickerKey,
        initialDate: DateTime(2016, DateTime.january, 15),
        initialCalendarMode: DatePickerMode.year,
      ));
      await tester.pumpAndSettle();

      // Should be in year mode.
      expect(find.text('January 2016'), findsOneWidget); // Day/year selector
      expect(find.text('15'), findsNothing); // day 15 in grid
      expect(find.text('2016'), findsOneWidget); // 2016 in year grid

      await tester.pumpWidget(calendarDatePicker(
        key: pickerKey,
        initialDate: DateTime(2016, DateTime.january, 15),
      ));
      await tester.pumpAndSettle();

      // Should be in year mode still; updating an _initial_ parameter has no effect.
      expect(find.text('January 2016'), findsOneWidget); // Day/year selector
      expect(find.text('15'), findsNothing); // day 15 in grid
      expect(find.text('2016'), findsOneWidget); // 2016 in year grid
    });

    testWidgetsWithLeakTracking('Dragging more than half the width should not cause a jump', (WidgetTester tester) async {
      await tester.pumpWidget(calendarDatePicker(
        initialDate: DateTime(2016, DateTime.january, 15),
      ));
      await tester.pumpAndSettle();
      final TestGesture gesture = await tester.startGesture(tester.getCenter(find.byType(PageView)));
      // This initial drag is required for the PageView to recognize the gesture, as it uses DragStartBehavior.start.
      // It does not count towards the drag distance.
      await gesture.moveBy(const Offset(100, 0));
      // Dragging for a bit less than half the width should reveal the previous month.
      await gesture.moveBy(const Offset(800 / 2 - 1, 0));
      await tester.pumpAndSettle();
      expect(find.text('January 2016'), findsOneWidget);
      expect(find.text('1'), findsNWidgets(2));
      // Dragging a bit over the half should still show both.
      await gesture.moveBy(const Offset(2, 0));
      await tester.pumpAndSettle();
      expect(find.text('December 2015'), findsOneWidget);
      expect(find.text('1'), findsNWidgets(2));
    });

    group('Keyboard navigation', () {
      testWidgetsWithLeakTracking('Can toggle to year mode', (WidgetTester tester) async {
        await tester.pumpWidget(calendarDatePicker(
          initialDate: DateTime(2016, DateTime.january, 15),
        ));
        expect(find.text('2016'), findsNothing);
        expect(find.text('January 2016'), findsOneWidget);
        // Navigate to the year selector and activate it.
        await tester.sendKeyEvent(LogicalKeyboardKey.tab);
        await tester.sendKeyEvent(LogicalKeyboardKey.space);
        await tester.pumpAndSettle();
        // The years should be visible.
        expect(find.text('2016'), findsOneWidget);
        expect(find.text('January 2016'), findsOneWidget);
      });

      testWidgetsWithLeakTracking('Can navigate next/previous months', (WidgetTester tester) async {
        await tester.pumpWidget(calendarDatePicker(
          initialDate: DateTime(2016, DateTime.january, 15),
        ));
        expect(find.text('January 2016'), findsOneWidget);
        // Navigate to the previous month button and activate it twice.
        await tester.sendKeyEvent(LogicalKeyboardKey.tab);
        await tester.sendKeyEvent(LogicalKeyboardKey.tab);
        await tester.sendKeyEvent(LogicalKeyboardKey.space);
        await tester.pumpAndSettle();
        await tester.sendKeyEvent(LogicalKeyboardKey.space);
        await tester.pumpAndSettle();
        // Should be showing Nov 2015
        expect(find.text('November 2015'), findsOneWidget);

        // Navigate to the next month button and activate it four times.
        await tester.sendKeyEvent(LogicalKeyboardKey.tab);
        await tester.sendKeyEvent(LogicalKeyboardKey.space);
        await tester.pumpAndSettle();
        await tester.sendKeyEvent(LogicalKeyboardKey.space);
        await tester.pumpAndSettle();
        await tester.sendKeyEvent(LogicalKeyboardKey.space);
        await tester.pumpAndSettle();
        await tester.sendKeyEvent(LogicalKeyboardKey.space);
        await tester.pumpAndSettle();
        // Should be on Mar 2016.
        expect(find.text('March 2016'), findsOneWidget);
      });

      testWidgetsWithLeakTracking('Can navigate date grid with arrow keys', (WidgetTester tester) async {
        DateTime? selectedDate;
        await tester.pumpWidget(calendarDatePicker(
          initialDate: DateTime(2016, DateTime.january, 15),
          onDateChanged: (DateTime date) => selectedDate = date,
        ));
        // Navigate to the grid.
        await tester.sendKeyEvent(LogicalKeyboardKey.tab);
        await tester.sendKeyEvent(LogicalKeyboardKey.tab);
        await tester.sendKeyEvent(LogicalKeyboardKey.tab);
        await tester.sendKeyEvent(LogicalKeyboardKey.tab);

        // Navigate from Jan 15 to Jan 18 with arrow keys.
        await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft);
        await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
        await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft);
        await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
        await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
        await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft);
        await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft);
        await tester.pumpAndSettle();

        // Activate it.
        await tester.sendKeyEvent(LogicalKeyboardKey.space);
        await tester.pumpAndSettle();

        // Should have selected Jan 18.
        expect(selectedDate, DateTime(2016, DateTime.january, 18));
      });

      testWidgetsWithLeakTracking('Navigating with arrow keys scrolls months', (WidgetTester tester) async {
        DateTime? selectedDate;
        await tester.pumpWidget(calendarDatePicker(
          initialDate: DateTime(2016, DateTime.january, 15),
          onDateChanged: (DateTime date) => selectedDate = date,
        ));
        // Navigate to the grid.
        await tester.sendKeyEvent(LogicalKeyboardKey.tab);
        await tester.sendKeyEvent(LogicalKeyboardKey.tab);
        await tester.sendKeyEvent(LogicalKeyboardKey.tab);
        await tester.sendKeyEvent(LogicalKeyboardKey.tab);
        await tester.pumpAndSettle();

        // Navigate from Jan 15 to Dec 31 with arrow keys
        await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
        await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
        await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft);
        await tester.pumpAndSettle();

        // Should have scrolled to Dec 2015.
        expect(find.text('December 2015'), findsOneWidget);

        // Navigate from Dec 31 to Nov 26 with arrow keys.
        await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
        await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
        await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
        await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
        await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
        await tester.pumpAndSettle();

        // Should have scrolled to Nov 2015.
        expect(find.text('November 2015'), findsOneWidget);

        // Activate it
        await tester.sendKeyEvent(LogicalKeyboardKey.space);
        await tester.pumpAndSettle();

        // Should have selected Jan 18.
        expect(selectedDate, DateTime(2015, DateTime.november, 26));
      });

      testWidgetsWithLeakTracking('RTL text direction reverses the horizontal arrow key navigation', (WidgetTester tester) async {
        DateTime? selectedDate;
        await tester.pumpWidget(calendarDatePicker(
          initialDate: DateTime(2016, DateTime.january, 15),
          onDateChanged: (DateTime date) => selectedDate = date,
          textDirection: TextDirection.rtl,
        ));
        // Navigate to the grid.
        await tester.sendKeyEvent(LogicalKeyboardKey.tab);
        await tester.sendKeyEvent(LogicalKeyboardKey.tab);
        await tester.sendKeyEvent(LogicalKeyboardKey.tab);
        await tester.sendKeyEvent(LogicalKeyboardKey.tab);
        await tester.pumpAndSettle();

        // Navigate from Jan 15 to 19 with arrow keys.
        await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
        await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
        await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
        await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
        await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
        await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft);
        await tester.pumpAndSettle();

        // Activate it.
        await tester.sendKeyEvent(LogicalKeyboardKey.space);
        await tester.pumpAndSettle();

        // Should have selected Jan 19.
        expect(selectedDate, DateTime(2016, DateTime.january, 19));
      });
    });

    group('Haptic feedback', () {
      const Duration hapticFeedbackInterval = Duration(milliseconds: 10);
      late FeedbackTester feedback;

      setUp(() {
        feedback = FeedbackTester();
      });

      tearDown(() {
        feedback.dispose();
      });

      testWidgetsWithLeakTracking('Selecting date vibrates', (WidgetTester tester) async {
        await tester.pumpWidget(calendarDatePicker(
          initialDate: DateTime(2016, DateTime.january, 15),
        ));
        await tester.tap(find.text('10'));
        await tester.pump(hapticFeedbackInterval);
        expect(feedback.hapticCount, 1);
        await tester.tap(find.text('12'));
        await tester.pump(hapticFeedbackInterval);
        expect(feedback.hapticCount, 2);
        await tester.tap(find.text('14'));
        await tester.pump(hapticFeedbackInterval);
        expect(feedback.hapticCount, 3);
      });

      testWidgetsWithLeakTracking('Tapping unselectable date does not vibrate', (WidgetTester tester) async {
        await tester.pumpWidget(calendarDatePicker(
          initialDate: DateTime(2016, DateTime.january, 10),
          selectableDayPredicate: (DateTime date) => date.day.isEven,
        ));
        await tester.tap(find.text('11'));
        await tester.pump(hapticFeedbackInterval);
        expect(feedback.hapticCount, 0);
        await tester.tap(find.text('13'));
        await tester.pump(hapticFeedbackInterval);
        expect(feedback.hapticCount, 0);
        await tester.tap(find.text('15'));
        await tester.pump(hapticFeedbackInterval);
        expect(feedback.hapticCount, 0);
      });

      testWidgetsWithLeakTracking('Changing modes and year vibrates', (WidgetTester tester) async {
        await tester.pumpWidget(calendarDatePicker(
          initialDate: DateTime(2016, DateTime.january, 15),
        ));
        await tester.tap(find.text('January 2016'));
        await tester.pump(hapticFeedbackInterval);
        expect(feedback.hapticCount, 1);
        await tester.tap(find.text('2018'));
        await tester.pump(hapticFeedbackInterval);
        expect(feedback.hapticCount, 2);
      });
    });

    group('Semantics', () {
      testWidgetsWithLeakTracking('day mode', (WidgetTester tester) async {
        final SemanticsHandle semantics = tester.ensureSemantics();

        await tester.pumpWidget(calendarDatePicker(
          initialDate: DateTime(2016, DateTime.january, 15),
        ));

        // Year mode drop down button.
        expect(tester.getSemantics(find.text('January 2016')), matchesSemantics(
          label: 'Select year',
          isButton: true,
        ));

        // Prev/Next month buttons.
        expect(tester.getSemantics(previousMonthIcon), matchesSemantics(
          tooltip: 'Previous month',
          isButton: true,
          hasTapAction: true,
          isEnabled: true,
          hasEnabledState: true,
          isFocusable: true,
        ));
        expect(tester.getSemantics(nextMonthIcon), matchesSemantics(
          tooltip: 'Next month',
          isButton: true,
          hasTapAction: true,
          isEnabled: true,
          hasEnabledState: true,
          isFocusable: true,
        ));

        // Day grid.
        expect(tester.getSemantics(find.text('1')), matchesSemantics(
          label: '1, Friday, January 1, 2016',
          isButton: true,
          hasTapAction: true,
          isFocusable: true,
        ));
        expect(tester.getSemantics(find.text('2')), matchesSemantics(
          label: '2, Saturday, January 2, 2016',
          isButton: true,
          hasTapAction: true,
          isFocusable: true,
        ));
        expect(tester.getSemantics(find.text('3')), matchesSemantics(
          label: '3, Sunday, January 3, 2016, Today',
          isButton: true,
          hasTapAction: true,
          isFocusable: true,
        ));
        expect(tester.getSemantics(find.text('4')), matchesSemantics(
          label: '4, Monday, January 4, 2016',
          isButton: true,
          hasTapAction: true,
          isFocusable: true,
        ));
        expect(tester.getSemantics(find.text('5')), matchesSemantics(
          label: '5, Tuesday, January 5, 2016',
          isButton: true,
          hasTapAction: true,
          isFocusable: true,
        ));
        expect(tester.getSemantics(find.text('6')), matchesSemantics(
          label: '6, Wednesday, January 6, 2016',
          isButton: true,
          hasTapAction: true,
          isFocusable: true,
        ));
        expect(tester.getSemantics(find.text('7')), matchesSemantics(
          label: '7, Thursday, January 7, 2016',
          isButton: true,
          hasTapAction: true,
          isFocusable: true,
        ));
        expect(tester.getSemantics(find.text('8')), matchesSemantics(
          label: '8, Friday, January 8, 2016',
          isButton: true,
          hasTapAction: true,
          isFocusable: true,
        ));
        expect(tester.getSemantics(find.text('9')), matchesSemantics(
          label: '9, Saturday, January 9, 2016',
          isButton: true,
          hasTapAction: true,
          isFocusable: true,
        ));
        expect(tester.getSemantics(find.text('10')), matchesSemantics(
          label: '10, Sunday, January 10, 2016',
          isButton: true,
          hasTapAction: true,
          isFocusable: true,
        ));
        expect(tester.getSemantics(find.text('11')), matchesSemantics(
          label: '11, Monday, January 11, 2016',
          isButton: true,
          hasTapAction: true,
          isFocusable: true,
        ));
        expect(tester.getSemantics(find.text('12')), matchesSemantics(
          label: '12, Tuesday, January 12, 2016',
          isButton: true,
          hasTapAction: true,
          isFocusable: true,
        ));
        expect(tester.getSemantics(find.text('13')), matchesSemantics(
          label: '13, Wednesday, January 13, 2016',
          isButton: true,
          hasTapAction: true,
          isFocusable: true,
        ));
        expect(tester.getSemantics(find.text('14')), matchesSemantics(
          label: '14, Thursday, January 14, 2016',
          isButton: true,
          hasTapAction: true,
          isFocusable: true,
        ));
        expect(tester.getSemantics(find.text('15')), matchesSemantics(
          label: '15, Friday, January 15, 2016',
          isButton: true,
          hasTapAction: true,
          isSelected: true,
          isFocusable: true,
        ));
        expect(tester.getSemantics(find.text('16')), matchesSemantics(
          label: '16, Saturday, January 16, 2016',
          isButton: true,
          hasTapAction: true,
          isFocusable: true,
        ));
        expect(tester.getSemantics(find.text('17')), matchesSemantics(
          label: '17, Sunday, January 17, 2016',
          isButton: true,
          hasTapAction: true,
          isFocusable: true,
        ));
        expect(tester.getSemantics(find.text('18')), matchesSemantics(
          label: '18, Monday, January 18, 2016',
          isButton: true,
          hasTapAction: true,
          isFocusable: true,
        ));
        expect(tester.getSemantics(find.text('19')), matchesSemantics(
          label: '19, Tuesday, January 19, 2016',
          isButton: true,
          hasTapAction: true,
          isFocusable: true,
        ));
        expect(tester.getSemantics(find.text('20')), matchesSemantics(
          label: '20, Wednesday, January 20, 2016',
          isButton: true,
          hasTapAction: true,
          isFocusable: true,
        ));
        expect(tester.getSemantics(find.text('21')), matchesSemantics(
          label: '21, Thursday, January 21, 2016',
          isButton: true,
          hasTapAction: true,
          isFocusable: true,
        ));
        expect(tester.getSemantics(find.text('22')), matchesSemantics(
          label: '22, Friday, January 22, 2016',
          isButton: true,
          hasTapAction: true,
          isFocusable: true,
        ));
        expect(tester.getSemantics(find.text('23')), matchesSemantics(
          label: '23, Saturday, January 23, 2016',
          isButton: true,
          hasTapAction: true,
          isFocusable: true,
        ));
        expect(tester.getSemantics(find.text('24')), matchesSemantics(
          label: '24, Sunday, January 24, 2016',
          isButton: true,
          hasTapAction: true,
          isFocusable: true,
        ));
        expect(tester.getSemantics(find.text('25')), matchesSemantics(
          label: '25, Monday, January 25, 2016',
          isButton: true,
          hasTapAction: true,
          isFocusable: true,
        ));
        expect(tester.getSemantics(find.text('26')), matchesSemantics(
          label: '26, Tuesday, January 26, 2016',
          isButton: true,
          hasTapAction: true,
          isFocusable: true,
        ));
        expect(tester.getSemantics(find.text('27')), matchesSemantics(
          label: '27, Wednesday, January 27, 2016',
          isButton: true,
          hasTapAction: true,
          isFocusable: true,
        ));
        expect(tester.getSemantics(find.text('28')), matchesSemantics(
          label: '28, Thursday, January 28, 2016',
          isButton: true,
          hasTapAction: true,
          isFocusable: true,
        ));
        expect(tester.getSemantics(find.text('29')), matchesSemantics(
          label: '29, Friday, January 29, 2016',
          isButton: true,
          hasTapAction: true,
          isFocusable: true,
        ));
        expect(tester.getSemantics(find.text('30')), matchesSemantics(
          label: '30, Saturday, January 30, 2016',
          isButton: true,
          hasTapAction: true,
          isFocusable: true,
        ));
        semantics.dispose();
      });

      testWidgetsWithLeakTracking('calendar year mode', (WidgetTester tester) async {
        final SemanticsHandle semantics = tester.ensureSemantics();

        await tester.pumpWidget(calendarDatePicker(
          initialDate: DateTime(2016, DateTime.january, 15),
          initialCalendarMode: DatePickerMode.year,
        ));

        // Year mode drop down button.
        expect(tester.getSemantics(find.text('January 2016')), matchesSemantics(
          label: 'Select year',
          isButton: true,
        ));

        // Year grid only shows 2010 - 2024.
        for (int year = 2010; year <= 2024; year++) {
          expect(tester.getSemantics(find.text('$year')), matchesSemantics(
            label: '$year',
            hasTapAction: true,
            isSelected: year == 2016,
            isFocusable: true,
            isButton: true,
          ));
        }
        semantics.dispose();
      });
    });
  });

  group('YearPicker', () {
    testWidgetsWithLeakTracking('Current year is visible in year picker', (WidgetTester tester) async {
      await tester.pumpWidget(yearPicker());
      expect(find.text('2016'), findsOneWidget);
    });

    testWidgetsWithLeakTracking('Can select a year', (WidgetTester tester) async {
      DateTime? selectedDate;
      await tester.pumpWidget(yearPicker(
        onChanged: (DateTime date) => selectedDate = date,
      ));
      await tester.pumpAndSettle();
      await tester.tap(find.text('2018'));
      await tester.pumpAndSettle();
      expect(selectedDate, equals(DateTime(2018)));
    });

    testWidgetsWithLeakTracking('Cannot select disabled year', (WidgetTester tester) async {
      DateTime? selectedYear;
      await tester.pumpWidget(yearPicker(
        firstDate: DateTime(2018, DateTime.june, 9),
        selectedDate: DateTime(2018, DateTime.july, 4),
        lastDate: DateTime(2018, DateTime.december, 15),
        onChanged: (DateTime date) => selectedYear = date,
      ));
      await tester.tap(find.text('2016')); // Disabled, doesn't change the year.
      await tester.pumpAndSettle();
      expect(selectedYear, isNull);
      await tester.tap(find.text('2020')); // Disabled, doesn't change the year.
      await tester.pumpAndSettle();
      expect(selectedYear, isNull);
      await tester.tap(find.text('2018'));
      await tester.pumpAndSettle();
      expect(selectedYear, equals(DateTime(2018, DateTime.july)));
    });

    testWidgetsWithLeakTracking('Selecting year with no selected month uses earliest month', (WidgetTester tester) async {
      DateTime? selectedYear;
      await tester.pumpWidget(yearPicker(
        firstDate: DateTime(2018, DateTime.june, 9),
        lastDate: DateTime(2019, DateTime.december, 15),
        onChanged: (DateTime date) => selectedYear = date,
      ));
      await tester.tap(find.text('2018'));
      expect(selectedYear, equals(DateTime(2018, DateTime.june)));
      await tester.pumpWidget(yearPicker(
        firstDate: DateTime(2018, DateTime.june, 9),
        lastDate: DateTime(2019, DateTime.december, 15),
        selectedDate: DateTime(2018, DateTime.june),
        onChanged: (DateTime date) => selectedYear = date,
      ));
      await tester.tap(find.text('2019'));
      expect(selectedYear, equals(DateTime(2019, DateTime.june)));
    });

    testWidgetsWithLeakTracking('Selecting year with no selected month uses January', (WidgetTester tester) async {
      DateTime? selectedYear;
      await tester.pumpWidget(yearPicker(
        firstDate: DateTime(2018, DateTime.june, 9),
        lastDate: DateTime(2019, DateTime.december, 15),
        onChanged: (DateTime date) => selectedYear = date,
      ));
      await tester.tap(find.text('2019'));
      expect(selectedYear, equals(DateTime(2019))); // january implied
      await tester.pumpWidget(yearPicker(
        firstDate: DateTime(2018, DateTime.june, 9),
        lastDate: DateTime(2019, DateTime.december, 15),
        selectedDate: DateTime(2018),
        onChanged: (DateTime date) => selectedYear = date,
      ));
      await tester.tap(find.text('2018'));
      expect(selectedYear, equals(DateTime(2018, DateTime.june)));
    });
  });
}