// 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.

@TestOn('!chrome')
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';

import '../widgets/semantics_tester.dart';
import 'feedback_tester.dart';

final Finder _hourControl = find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_HourControl');
final Finder _minuteControl = find.byWidgetPredicate((Widget widget) => '${widget.runtimeType}' == '_MinuteControl');
final Finder _timePickerDialog = find.byWidgetPredicate((Widget widget) => '${widget.runtimeType}' == 'TimePickerDialog');

class _TimePickerLauncher extends StatefulWidget {
  const _TimePickerLauncher({
    Key? key,
    required this.onChanged,
    this.entryMode = TimePickerEntryMode.dial,
    this.restorationId,
  }) : super(key: key);

  final ValueChanged<TimeOfDay?> onChanged;
  final TimePickerEntryMode entryMode;
  final String? restorationId;

  @override
  _TimePickerLauncherState createState() => _TimePickerLauncherState();
}

class _TimePickerLauncherState extends State<_TimePickerLauncher> with RestorationMixin {
  @override
  String? get restorationId => widget.restorationId;

  late final RestorableRouteFuture<TimeOfDay?> _restorableTimePickerRouteFuture = RestorableRouteFuture<TimeOfDay?>(
    onComplete: _selectTime,
    onPresent: (NavigatorState navigator, Object? arguments) {
      return navigator.restorablePush(
        _timePickerRoute,
        arguments: <String, int>{
          'entryMode': widget.entryMode.index,
        },
      );
    },
  );

  static Route<TimeOfDay> _timePickerRoute(
    BuildContext context,
    Object? arguments,
  ) {
    final Map<dynamic, dynamic> args = arguments! as Map<dynamic, dynamic>;
    final TimePickerEntryMode entryMode = TimePickerEntryMode.values[args['entryMode'] as int];
    return DialogRoute<TimeOfDay>(
      context: context,
      builder: (BuildContext context) {
        return TimePickerDialog(
          restorationId: 'time_picker_dialog',
          initialTime: const TimeOfDay(hour: 7, minute: 0),
          initialEntryMode: entryMode,
        );
      },
    );
  }

  @override
  void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
    registerForRestoration(_restorableTimePickerRouteFuture, 'time_picker_route_future');
  }

  void _selectTime(TimeOfDay? newSelectedTime) {
    widget.onChanged(newSelectedTime);
  }

  @override
  Widget build(BuildContext context) {
    return Material(
      child: Center(
        child: Builder(
          builder: (BuildContext context) {
            return ElevatedButton(
              child: const Text('X'),
              onPressed: () async {
                if (widget.restorationId == null) {
                  widget.onChanged(await showTimePicker(
                    context: context,
                    initialTime: const TimeOfDay(hour: 7, minute: 0),
                    initialEntryMode: widget.entryMode,
                  ));
                } else {
                  _restorableTimePickerRouteFuture.present();
                }
              },
            );
          },
        ),
      ),
    );
  }
}

Future<Offset?> startPicker(
    WidgetTester tester,
    ValueChanged<TimeOfDay?> onChanged, {
      TimePickerEntryMode entryMode = TimePickerEntryMode.dial,
      String? restorationId,
    }) async {
  await tester.pumpWidget(MaterialApp(
    restorationScopeId: 'app',
    locale: const Locale('en', 'US'),
    home: _TimePickerLauncher(
      onChanged: onChanged,
      entryMode: entryMode,
      restorationId: restorationId,
    ),
  ));
  await tester.tap(find.text('X'));
  await tester.pumpAndSettle(const Duration(seconds: 1));
  return entryMode == TimePickerEntryMode.dial ? tester.getCenter(find.byKey(const ValueKey<String>('time-picker-dial'))) : null;
}

Future<void> finishPicker(WidgetTester tester) async {
  final MaterialLocalizations materialLocalizations = MaterialLocalizations.of(tester.element(find.byType(ElevatedButton)));
  await tester.tap(find.text(materialLocalizations.okButtonLabel));
  await tester.pumpAndSettle(const Duration(seconds: 1));
}

void main() {
  group('Time picker - Dial', () {
    _tests();
  });

  group('Time picker - Input', () {
    _testsInput();
  });
}

void _tests() {
  testWidgets('tap-select an hour', (WidgetTester tester) async {
    TimeOfDay? result;

    Offset center = (await startPicker(tester, (TimeOfDay? time) { result = time; }))!;
    await tester.tapAt(Offset(center.dx, center.dy - 50.0)); // 12:00 AM
    await finishPicker(tester);
    expect(result, equals(const TimeOfDay(hour: 0, minute: 0)));

    center = (await startPicker(tester, (TimeOfDay? time) { result = time; }))!;
    await tester.tapAt(Offset(center.dx + 50.0, center.dy));
    await finishPicker(tester);
    expect(result, equals(const TimeOfDay(hour: 3, minute: 0)));

    center = (await startPicker(tester, (TimeOfDay? time) { result = time; }))!;
    await tester.tapAt(Offset(center.dx, center.dy + 50.0));
    await finishPicker(tester);
    expect(result, equals(const TimeOfDay(hour: 6, minute: 0)));

    center = (await startPicker(tester, (TimeOfDay? time) { result = time; }))!;
    await tester.tapAt(Offset(center.dx, center.dy + 50.0));
    await tester.tapAt(Offset(center.dx - 50, center.dy));
    await finishPicker(tester);
    expect(result, equals(const TimeOfDay(hour: 9, minute: 0)));
  });

  testWidgets('drag-select an hour', (WidgetTester tester) async {
    late TimeOfDay result;

    final Offset center = (await startPicker(tester, (TimeOfDay? time) { result = time!; }))!;
    final Offset hour0 = Offset(center.dx, center.dy - 50.0); // 12:00 AM
    final Offset hour3 = Offset(center.dx + 50.0, center.dy);
    final Offset hour6 = Offset(center.dx, center.dy + 50.0);
    final Offset hour9 = Offset(center.dx - 50.0, center.dy);

    TestGesture gesture;

    gesture = await tester.startGesture(hour3);
    await gesture.moveBy(hour0 - hour3);
    await gesture.up();
    await finishPicker(tester);
    expect(result.hour, 0);

    expect(await startPicker(tester, (TimeOfDay? time) { result = time!; }), equals(center));
    gesture = await tester.startGesture(hour0);
    await gesture.moveBy(hour3 - hour0);
    await gesture.up();
    await finishPicker(tester);
    expect(result.hour, 3);

    expect(await startPicker(tester, (TimeOfDay? time) { result = time!; }), equals(center));
    gesture = await tester.startGesture(hour3);
    await gesture.moveBy(hour6 - hour3);
    await gesture.up();
    await finishPicker(tester);
    expect(result.hour, equals(6));

    expect(await startPicker(tester, (TimeOfDay? time) { result = time!; }), equals(center));
    gesture = await tester.startGesture(hour6);
    await gesture.moveBy(hour9 - hour6);
    await gesture.up();
    await finishPicker(tester);
    expect(result.hour, equals(9));
  });

  testWidgets('tap-select switches from hour to minute', (WidgetTester tester) async {
    late TimeOfDay result;

    final Offset center = (await startPicker(tester, (TimeOfDay? time) { result = time!; }))!;
    final Offset hour6 = Offset(center.dx, center.dy + 50.0); // 6:00
    final Offset min45 = Offset(center.dx - 50.0, center.dy); // 45 mins (or 9:00 hours)

    await tester.tapAt(hour6);
    await tester.pump(const Duration(milliseconds: 50));
    await tester.tapAt(min45);
    await finishPicker(tester);
    expect(result, equals(const TimeOfDay(hour: 6, minute: 45)));
  });

  testWidgets('drag-select switches from hour to minute', (WidgetTester tester) async {
    late TimeOfDay result;

    final Offset center = (await startPicker(tester, (TimeOfDay? time) { result = time!; }))!;
    final Offset hour3 = Offset(center.dx + 50.0, center.dy);
    final Offset hour6 = Offset(center.dx, center.dy + 50.0);
    final Offset hour9 = Offset(center.dx - 50.0, center.dy);

    TestGesture gesture = await tester.startGesture(hour6);
    await gesture.moveBy(hour9 - hour6);
    await gesture.up();
    await tester.pump(const Duration(milliseconds: 50));
    gesture = await tester.startGesture(hour6);
    await gesture.moveBy(hour3 - hour6);
    await gesture.up();
    await finishPicker(tester);
    expect(result, equals(const TimeOfDay(hour: 9, minute: 15)));
  });

  testWidgets('tap-select rounds down to nearest 5 minute increment', (WidgetTester tester) async {
    late TimeOfDay result;

    final Offset center = (await startPicker(tester, (TimeOfDay? time) { result = time!; }))!;
    final Offset hour6 = Offset(center.dx, center.dy + 50.0); // 6:00
    final Offset min46 = Offset(center.dx - 50.0, center.dy - 5); // 46 mins

    await tester.tapAt(hour6);
    await tester.pump(const Duration(milliseconds: 50));
    await tester.tapAt(min46);
    await finishPicker(tester);
    expect(result, equals(const TimeOfDay(hour: 6, minute: 45)));
  });

  testWidgets('tap-select rounds up to nearest 5 minute increment', (WidgetTester tester) async {
    late TimeOfDay result;

    final Offset center = (await startPicker(tester, (TimeOfDay? time) { result = time!; }))!;
    final Offset hour6 = Offset(center.dx, center.dy + 50.0); // 6:00
    final Offset min48 = Offset(center.dx - 50.0, center.dy - 15); // 48 mins

    await tester.tapAt(hour6);
    await tester.pump(const Duration(milliseconds: 50));
    await tester.tapAt(min48);
    await finishPicker(tester);
    expect(result, equals(const TimeOfDay(hour: 6, minute: 50)));
  });

  group('haptic feedback', () {
    const Duration kFastFeedbackInterval = Duration(milliseconds: 10);
    const Duration kSlowFeedbackInterval = Duration(milliseconds: 200);
    late FeedbackTester feedback;

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

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

    testWidgets('tap-select vibrates once', (WidgetTester tester) async {
      final Offset center = (await startPicker(tester, (TimeOfDay? time) { }))!;
      await tester.tapAt(Offset(center.dx, center.dy - 50.0));
      await finishPicker(tester);
      expect(feedback.hapticCount, 1);
    });

    testWidgets('quick successive tap-selects vibrate once', (WidgetTester tester) async {
      final Offset center = (await startPicker(tester, (TimeOfDay? time) { }))!;
      await tester.tapAt(Offset(center.dx, center.dy - 50.0));
      await tester.pump(kFastFeedbackInterval);
      await tester.tapAt(Offset(center.dx, center.dy + 50.0));
      await finishPicker(tester);
      expect(feedback.hapticCount, 1);
    });

    testWidgets('slow successive tap-selects vibrate once per tap', (WidgetTester tester) async {
      final Offset center = (await startPicker(tester, (TimeOfDay? time) { }))!;
      await tester.tapAt(Offset(center.dx, center.dy - 50.0));
      await tester.pump(kSlowFeedbackInterval);
      await tester.tapAt(Offset(center.dx, center.dy + 50.0));
      await tester.pump(kSlowFeedbackInterval);
      await tester.tapAt(Offset(center.dx, center.dy - 50.0));
      await finishPicker(tester);
      expect(feedback.hapticCount, 3);
    });

    testWidgets('drag-select vibrates once', (WidgetTester tester) async {
      final Offset center = (await startPicker(tester, (TimeOfDay? time) { }))!;
      final Offset hour0 = Offset(center.dx, center.dy - 50.0);
      final Offset hour3 = Offset(center.dx + 50.0, center.dy);

      final TestGesture gesture = await tester.startGesture(hour3);
      await gesture.moveBy(hour0 - hour3);
      await gesture.up();
      await finishPicker(tester);
      expect(feedback.hapticCount, 1);
    });

    testWidgets('quick drag-select vibrates once', (WidgetTester tester) async {
      final Offset center = (await startPicker(tester, (TimeOfDay? time) { }))!;
      final Offset hour0 = Offset(center.dx, center.dy - 50.0);
      final Offset hour3 = Offset(center.dx + 50.0, center.dy);

      final TestGesture gesture = await tester.startGesture(hour3);
      await gesture.moveBy(hour0 - hour3);
      await tester.pump(kFastFeedbackInterval);
      await gesture.moveBy(hour3 - hour0);
      await tester.pump(kFastFeedbackInterval);
      await gesture.moveBy(hour0 - hour3);
      await gesture.up();
      await finishPicker(tester);
      expect(feedback.hapticCount, 1);
    });

    testWidgets('slow drag-select vibrates once', (WidgetTester tester) async {
      final Offset center = (await startPicker(tester, (TimeOfDay? time) { }))!;
      final Offset hour0 = Offset(center.dx, center.dy - 50.0);
      final Offset hour3 = Offset(center.dx + 50.0, center.dy);

      final TestGesture gesture = await tester.startGesture(hour3);
      await gesture.moveBy(hour0 - hour3);
      await tester.pump(kSlowFeedbackInterval);
      await gesture.moveBy(hour3 - hour0);
      await tester.pump(kSlowFeedbackInterval);
      await gesture.moveBy(hour0 - hour3);
      await gesture.up();
      await finishPicker(tester);
      expect(feedback.hapticCount, 3);
    });
  });

  const List<String> labels12To11 = <String>['12', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11'];
  const List<String> labels00To22 = <String>['00', '02', '04', '06', '08', '10', '12', '14', '16', '18', '20', '22'];

  testWidgets('respects MediaQueryData.alwaysUse24HourFormat == false', (WidgetTester tester) async {
    await mediaQueryBoilerplate(tester, false);

    final CustomPaint dialPaint = tester.widget(findDialPaint);
    final dynamic dialPainter = dialPaint.painter;
    // ignore: avoid_dynamic_calls
    final List<dynamic> primaryLabels = dialPainter.primaryLabels as List<dynamic>;
    // ignore: avoid_dynamic_calls
    expect(primaryLabels.map<String>((dynamic tp) => tp.painter.text.text as String), labels12To11);

    // ignore: avoid_dynamic_calls
    final List<dynamic> secondaryLabels = dialPainter.secondaryLabels as List<dynamic>;
    // ignore: avoid_dynamic_calls
    expect(secondaryLabels.map<String>((dynamic tp) => tp.painter.text.text as String), labels12To11);
  });

  testWidgets('respects MediaQueryData.alwaysUse24HourFormat == true', (WidgetTester tester) async {
    await mediaQueryBoilerplate(tester, true);

    final CustomPaint dialPaint = tester.widget(findDialPaint);
    final dynamic dialPainter = dialPaint.painter;
    // ignore: avoid_dynamic_calls
    final List<dynamic> primaryLabels = dialPainter.primaryLabels as List<dynamic>;
    // ignore: avoid_dynamic_calls
    expect(primaryLabels.map<String>((dynamic tp) => tp.painter.text.text as String), labels00To22);

    // ignore: avoid_dynamic_calls
    final List<dynamic> secondaryLabels = dialPainter.secondaryLabels as List<dynamic>;
    // ignore: avoid_dynamic_calls
    expect(secondaryLabels.map<String>((dynamic tp) => tp.painter.text.text as String), labels00To22);
  });

  testWidgets('provides semantics information for AM/PM indicator', (WidgetTester tester) async {
    final SemanticsTester semantics = SemanticsTester(tester);
    await mediaQueryBoilerplate(tester, false);

    expect(
      semantics,
      includesNodeWith(
        label: 'AM',
        actions: <SemanticsAction>[SemanticsAction.tap],
        flags: <SemanticsFlag>[
          SemanticsFlag.isButton,
          SemanticsFlag.isChecked,
          SemanticsFlag.isInMutuallyExclusiveGroup,
          SemanticsFlag.hasCheckedState,
          SemanticsFlag.isFocusable,
        ],
      ),
    );
    expect(
      semantics,
      includesNodeWith(
        label: 'PM',
        actions: <SemanticsAction>[SemanticsAction.tap],
        flags: <SemanticsFlag>[
          SemanticsFlag.isButton,
          SemanticsFlag.isInMutuallyExclusiveGroup,
          SemanticsFlag.hasCheckedState,
          SemanticsFlag.isFocusable,
        ],
      ),
    );

    semantics.dispose();
  });

  testWidgets('provides semantics information for header and footer', (WidgetTester tester) async {
    final SemanticsTester semantics = SemanticsTester(tester);
    await mediaQueryBoilerplate(tester, true);

    expect(semantics, isNot(includesNodeWith(label: ':')));
    expect(
      semantics.nodesWith(value: 'Select minutes 00'),
      hasLength(1),
      reason: '00 appears once in the header',
    );
    expect(
      semantics.nodesWith(value: 'Select hours 07'),
      hasLength(1),
      reason: '07 appears once in the header',
    );
    expect(semantics, includesNodeWith(label: 'CANCEL'));
    expect(semantics, includesNodeWith(label: 'OK'));

    // In 24-hour mode we don't have AM/PM control.
    expect(semantics, isNot(includesNodeWith(label: 'AM')));
    expect(semantics, isNot(includesNodeWith(label: 'PM')));

    semantics.dispose();
  });

  testWidgets('provides semantics information for text fields', (WidgetTester tester) async {
    final SemanticsTester semantics = SemanticsTester(tester);
    await mediaQueryBoilerplate(tester, true, entryMode: TimePickerEntryMode.input, accessibleNavigation: true);

    expect(
      semantics,
      includesNodeWith(
        label: 'Hour',
        value: '07',
        actions: <SemanticsAction>[SemanticsAction.tap],
        flags: <SemanticsFlag>[SemanticsFlag.isTextField, SemanticsFlag.isMultiline],
      ),
    );
    expect(
      semantics,
      includesNodeWith(
        label: 'Minute',
        value: '00',
        actions: <SemanticsAction>[SemanticsAction.tap],
        flags: <SemanticsFlag>[SemanticsFlag.isTextField, SemanticsFlag.isMultiline],
      ),
    );

    semantics.dispose();
  });

  testWidgets('can increment and decrement hours', (WidgetTester tester) async {
    final SemanticsTester semantics = SemanticsTester(tester);

    Future<void> actAndExpect({ required String initialValue, required SemanticsAction action, required String finalValue }) async {
      final SemanticsNode elevenHours = semantics.nodesWith(
        value: 'Select hours $initialValue',
        ancestor: tester.renderObject(_hourControl).debugSemantics,
      ).single;
      tester.binding.pipelineOwner.semanticsOwner!.performAction(elevenHours.id, action);
      await tester.pumpAndSettle();
      expect(
        find.descendant(of: _hourControl, matching: find.text(finalValue)),
        findsOneWidget,
      );
    }

    // 12-hour format
    await mediaQueryBoilerplate(tester, false, initialTime: const TimeOfDay(hour: 11, minute: 0));
    await actAndExpect(
      initialValue: '11',
      action: SemanticsAction.increase,
      finalValue: '12',
    );
    await actAndExpect(
      initialValue: '12',
      action: SemanticsAction.increase,
      finalValue: '1',
    );

    // Ensure we preserve day period as we roll over.
    final dynamic pickerState = tester.state(_timePickerDialog);
    // ignore: avoid_dynamic_calls
    expect(pickerState.selectedTime.value, const TimeOfDay(hour: 1, minute: 0));

    await actAndExpect(
      initialValue: '1',
      action: SemanticsAction.decrease,
      finalValue: '12',
    );
    await tester.pumpWidget(Container()); // clear old boilerplate

    // 24-hour format
    await mediaQueryBoilerplate(tester, true, initialTime: const TimeOfDay(hour: 23, minute: 0));
    await actAndExpect(
      initialValue: '23',
      action: SemanticsAction.increase,
      finalValue: '00',
    );
    await actAndExpect(
      initialValue: '00',
      action: SemanticsAction.increase,
      finalValue: '01',
    );
    await actAndExpect(
      initialValue: '01',
      action: SemanticsAction.decrease,
      finalValue: '00',
    );
    await actAndExpect(
      initialValue: '00',
      action: SemanticsAction.decrease,
      finalValue: '23',
    );

    semantics.dispose();
  });

  testWidgets('can increment and decrement minutes', (WidgetTester tester) async {
    final SemanticsTester semantics = SemanticsTester(tester);

    Future<void> actAndExpect({ required String initialValue, required SemanticsAction action, required String finalValue }) async {
      final SemanticsNode elevenHours = semantics.nodesWith(
        value: 'Select minutes $initialValue',
        ancestor: tester.renderObject(_minuteControl).debugSemantics,
      ).single;
      tester.binding.pipelineOwner.semanticsOwner!.performAction(elevenHours.id, action);
      await tester.pumpAndSettle();
      expect(
        find.descendant(of: _minuteControl, matching: find.text(finalValue)),
        findsOneWidget,
      );
    }

    await mediaQueryBoilerplate(tester, false, initialTime: const TimeOfDay(hour: 11, minute: 58));
    await actAndExpect(
      initialValue: '58',
      action: SemanticsAction.increase,
      finalValue: '59',
    );
    await actAndExpect(
      initialValue: '59',
      action: SemanticsAction.increase,
      finalValue: '00',
    );

    // Ensure we preserve hour period as we roll over.
    final dynamic pickerState = tester.state(_timePickerDialog);
    // ignore: avoid_dynamic_calls
    expect(pickerState.selectedTime.value, const TimeOfDay(hour: 11, minute: 0));

    await actAndExpect(
      initialValue: '00',
      action: SemanticsAction.decrease,
      finalValue: '59',
    );
    await actAndExpect(
      initialValue: '59',
      action: SemanticsAction.decrease,
      finalValue: '58',
    );

    semantics.dispose();
  });

  testWidgets('header touch regions are large enough', (WidgetTester tester) async {
    // Ensure picker is displayed in portrait mode.
    tester.binding.window.physicalSizeTestValue = const Size(400, 800);
    tester.binding.window.devicePixelRatioTestValue = 1;
    await mediaQueryBoilerplate(tester, false);

    final Size dayPeriodControlSize = tester.getSize(find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_DayPeriodControl'));
    expect(dayPeriodControlSize.width, greaterThanOrEqualTo(48.0));
    // Height should be double the minimum size to account for both AM/PM stacked.
    expect(dayPeriodControlSize.height, greaterThanOrEqualTo(48.0 * 2));

    final Size hourSize = tester.getSize(find.ancestor(
      of: find.text('7'),
      matching: find.byType(InkWell),
    ));
    expect(hourSize.width, greaterThanOrEqualTo(48.0));
    expect(hourSize.height, greaterThanOrEqualTo(48.0));

    final Size minuteSize = tester.getSize(find.ancestor(
      of: find.text('00'),
      matching: find.byType(InkWell),
    ));
    expect(minuteSize.width, greaterThanOrEqualTo(48.0));
    expect(minuteSize.height, greaterThanOrEqualTo(48.0));

    tester.binding.window.clearPhysicalSizeTestValue();
    tester.binding.window.clearDevicePixelRatioTestValue();
  });

  testWidgets('builder parameter', (WidgetTester tester) async {
    Widget buildFrame(TextDirection textDirection) {
      return MaterialApp(
        home: Material(
          child: Center(
            child: Builder(
              builder: (BuildContext context) {
                return ElevatedButton(
                  child: const Text('X'),
                  onPressed: () {
                    showTimePicker(
                      context: context,
                      initialTime: const TimeOfDay(hour: 7, minute: 0),
                      builder: (BuildContext context, Widget? child) {
                        return Directionality(
                          textDirection: textDirection,
                          child: child!,
                        );
                      },
                    );
                  },
                );
              },
            ),
          ),
        ),
      );
    }

    await tester.pumpWidget(buildFrame(TextDirection.ltr));
    await tester.tap(find.text('X'));
    await tester.pumpAndSettle();
    final double ltrOkRight = tester.getBottomRight(find.text('OK')).dx;

    await tester.tap(find.text('OK')); // dismiss the dialog
    await tester.pumpAndSettle();

    await tester.pumpWidget(buildFrame(TextDirection.rtl));
    await tester.tap(find.text('X'));
    await tester.pumpAndSettle();

    // Verify that the time picker is being laid out RTL.
    // We expect the left edge of the 'OK' button in the RTL
    // layout to match the gap between right edge of the 'OK'
    // button and the right edge of the 800 wide window.
    expect(tester.getBottomLeft(find.text('OK')).dx, 800 - ltrOkRight);
  });

  testWidgets('uses root navigator by default', (WidgetTester tester) async {
    final PickerObserver rootObserver = PickerObserver();
    final PickerObserver nestedObserver = PickerObserver();

    await tester.pumpWidget(MaterialApp(
      navigatorObservers: <NavigatorObserver>[rootObserver],
      home: Navigator(
        observers: <NavigatorObserver>[nestedObserver],
        onGenerateRoute: (RouteSettings settings) {
          return MaterialPageRoute<dynamic>(
            builder: (BuildContext context) {
              return ElevatedButton(
                onPressed: () {
                  showTimePicker(
                    context: context,
                    initialTime: const TimeOfDay(hour: 7, minute: 0),
                  );
                },
                child: const Text('Show Picker'),
              );
            },
          );
        },
      ),
    ));

    // Open the dialog.
    await tester.tap(find.byType(ElevatedButton));

    expect(rootObserver.pickerCount, 1);
    expect(nestedObserver.pickerCount, 0);
  });

  testWidgets('uses nested navigator if useRootNavigator is false', (WidgetTester tester) async {
    final PickerObserver rootObserver = PickerObserver();
    final PickerObserver nestedObserver = PickerObserver();

    await tester.pumpWidget(MaterialApp(
      navigatorObservers: <NavigatorObserver>[rootObserver],
      home: Navigator(
        observers: <NavigatorObserver>[nestedObserver],
        onGenerateRoute: (RouteSettings settings) {
          return MaterialPageRoute<dynamic>(
            builder: (BuildContext context) {
              return ElevatedButton(
                onPressed: () {
                  showTimePicker(
                    context: context,
                    useRootNavigator: false,
                    initialTime: const TimeOfDay(hour: 7, minute: 0),
                  );
                },
                child: const Text('Show Picker'),
              );
            },
          );
        },
      ),
    ));

    // Open the dialog.
    await tester.tap(find.byType(ElevatedButton));

    expect(rootObserver.pickerCount, 0);
    expect(nestedObserver.pickerCount, 1);
  });

  testWidgets('optional text parameters are utilized', (WidgetTester tester) async {
    const String cancelText = 'Custom Cancel';
    const String confirmText = 'Custom OK';
    const String helperText = 'Custom Help';
    await tester.pumpWidget(MaterialApp(
      home: Material(
        child: Center(
          child: Builder(
            builder: (BuildContext context) {
              return ElevatedButton(
                child: const Text('X'),
                onPressed: () async {
                  await showTimePicker(
                    context: context,
                    initialTime: const TimeOfDay(hour: 7, minute: 0),
                    cancelText: cancelText,
                    confirmText: confirmText,
                    helpText: helperText,
                  );
                },
              );
            },
          ),
        ),
      ),
    ));

    // Open the picker.
    await tester.tap(find.text('X'));
    await tester.pumpAndSettle(const Duration(seconds: 1));

    expect(find.text(cancelText), findsOneWidget);
    expect(find.text(confirmText), findsOneWidget);
    expect(find.text(helperText), findsOneWidget);
  });

  testWidgets('OK Cancel button layout', (WidgetTester tester) async {
    Widget buildFrame(TextDirection textDirection) {
      return MaterialApp(
        home: Material(
          child: Center(
            child: Builder(
              builder: (BuildContext context) {
                return ElevatedButton(
                  child: const Text('X'),
                  onPressed: () {
                    showTimePicker(
                      context: context,
                      initialTime: const TimeOfDay(hour: 7, minute: 0),
                      builder: (BuildContext context, Widget? child) {
                        return Directionality(
                          textDirection: textDirection,
                          child: child!,
                        );
                      },
                    );
                  },
                );
              },
            ),
          ),
        ),
      );
    }

    await tester.pumpWidget(buildFrame(TextDirection.ltr));
    await tester.tap(find.text('X'));
    await tester.pumpAndSettle();
    expect(tester.getBottomRight(find.text('OK')).dx, 638);
    expect(tester.getBottomLeft(find.text('OK')).dx, 610);
    expect(tester.getBottomRight(find.text('CANCEL')).dx, 576);
    await tester.tap(find.text('OK'));
    await tester.pumpAndSettle();

    await tester.pumpWidget(buildFrame(TextDirection.rtl));
    await tester.tap(find.text('X'));
    await tester.pumpAndSettle();
    expect(tester.getBottomLeft(find.text('OK')).dx, 162);
    expect(tester.getBottomRight(find.text('OK')).dx, 190);
    expect(tester.getBottomLeft(find.text('CANCEL')).dx, 224);
    await tester.tap(find.text('OK'));
    await tester.pumpAndSettle();
  });

  testWidgets('text scale affects certain elements and not others', (WidgetTester tester) async {
    await mediaQueryBoilerplate(
      tester,
      false,
      textScaleFactor: 1.0,
      initialTime: const TimeOfDay(hour: 7, minute: 41),
    );

    final double minutesDisplayHeight = tester.getSize(find.text('41')).height;
    final double amHeight = tester.getSize(find.text('AM')).height;

    await tester.tap(find.text('OK')); // dismiss the dialog
    await tester.pumpAndSettle();

    // Verify that the time display is not affected by text scale.
    await mediaQueryBoilerplate(
      tester,
      false,
      textScaleFactor: 2.0,
      initialTime: const TimeOfDay(hour: 7, minute: 41),
    );

    final double amHeight2x = tester.getSize(find.text('AM')).height;
    expect(tester.getSize(find.text('41')).height, equals(minutesDisplayHeight));
    expect(amHeight2x, greaterThanOrEqualTo(amHeight * 2));

    await tester.tap(find.text('OK')); // dismiss the dialog
    await tester.pumpAndSettle();

    // Verify that text scale for AM/PM is at most 2x.
    await mediaQueryBoilerplate(
      tester,
      false,
      textScaleFactor: 3.0,
      initialTime: const TimeOfDay(hour: 7, minute: 41),
    );

    expect(tester.getSize(find.text('41')).height, equals(minutesDisplayHeight));
    expect(tester.getSize(find.text('AM')).height, equals(amHeight2x));
  });
}

void _testsInput() {
  testWidgets('Initial entry mode is used', (WidgetTester tester) async {
    await mediaQueryBoilerplate(tester, true, entryMode: TimePickerEntryMode.input);
    expect(find.byType(TextField), findsNWidgets(2));
  });

  testWidgets('Initial time is the default', (WidgetTester tester) async {
    late TimeOfDay result;
    await startPicker(tester, (TimeOfDay? time) { result = time!; }, entryMode: TimePickerEntryMode.input);
    await finishPicker(tester);
    expect(result, equals(const TimeOfDay(hour: 7, minute: 0)));
  });

  testWidgets('Help text is used - Input', (WidgetTester tester) async {
    const String helpText = 'help';
    await mediaQueryBoilerplate(tester, true, entryMode: TimePickerEntryMode.input, helpText: helpText);
    expect(find.text(helpText), findsOneWidget);
  });

  testWidgets('Hour label text is used - Input', (WidgetTester tester) async {
    const String hourLabelText = 'Custom hour label';
    await mediaQueryBoilerplate(tester, true, entryMode: TimePickerEntryMode.input, hourLabelText: hourLabelText);
    expect(find.text(hourLabelText), findsOneWidget);
  });


  testWidgets('Minute label text is used - Input', (WidgetTester tester) async {
    const String minuteLabelText = 'Custom minute label';
    await mediaQueryBoilerplate(tester, true, entryMode: TimePickerEntryMode.input, minuteLabelText: minuteLabelText);
    expect(find.text(minuteLabelText), findsOneWidget);
  });

  testWidgets('Invalid error text is used - Input', (WidgetTester tester) async {
    const String errorInvalidText = 'Custom validation error';
    await mediaQueryBoilerplate(tester, true, entryMode: TimePickerEntryMode.input, errorInvalidText: errorInvalidText);
    // Input invalid time (hour) to force validation error
    await tester.enterText(find.byType(TextField).first, '88');
    final MaterialLocalizations materialLocalizations = MaterialLocalizations.of(tester.element(find.byType(TextButton).first));
    // Tap the ok button to trigger the validation error with custom translation
    await tester.tap(find.text(materialLocalizations.okButtonLabel));
    await tester.pumpAndSettle(const Duration(seconds: 1));
    expect(find.text(errorInvalidText), findsOneWidget);
  });

  testWidgets('Can toggle to dial entry mode', (WidgetTester tester) async {
    await mediaQueryBoilerplate(tester, true, entryMode: TimePickerEntryMode.input);
    await tester.tap(find.byIcon(Icons.access_time));
    await tester.pumpAndSettle();
    expect(find.byType(TextField), findsNothing);
  });

  testWidgets('Can double tap hours (when selected) to enter input mode', (WidgetTester tester) async {
    await mediaQueryBoilerplate(tester, false, entryMode: TimePickerEntryMode.dial);
    final Finder hourFinder = find.ancestor(
      of: find.text('7'),
      matching: find.byType(InkWell),
    );

    expect(find.byType(TextField), findsNothing);

    // Double tap the hour.
    await tester.tap(hourFinder);
    await tester.pump(const Duration(milliseconds: 100));
    await tester.tap(hourFinder);
    await tester.pumpAndSettle();

    expect(find.byType(TextField), findsWidgets);
  });

  testWidgets('Can not double tap hours (when not selected) to enter input mode', (WidgetTester tester) async {
    await mediaQueryBoilerplate(tester, false, entryMode: TimePickerEntryMode.dial);
    final Finder hourFinder = find.ancestor(
      of: find.text('7'),
      matching: find.byType(InkWell),
    );
    final Finder minuteFinder = find.ancestor(
      of: find.text('00'),
      matching: find.byType(InkWell),
    );

    expect(find.byType(TextField), findsNothing);

    // Switch to minutes mode.
    await tester.tap(minuteFinder);
    await tester.pumpAndSettle();

    // Double tap the hour.
    await tester.tap(hourFinder);
    await tester.pump(const Duration(milliseconds: 100));
    await tester.tap(hourFinder);
    await tester.pumpAndSettle();

    expect(find.byType(TextField), findsNothing);
  });

  testWidgets('Can double tap minutes (when selected) to enter input mode', (WidgetTester tester) async {
    await mediaQueryBoilerplate(tester, false, entryMode: TimePickerEntryMode.dial);
    final Finder minuteFinder = find.ancestor(
      of: find.text('00'),
      matching: find.byType(InkWell),
    );

    expect(find.byType(TextField), findsNothing);

    // Switch to minutes mode.
    await tester.tap(minuteFinder);
    await tester.pumpAndSettle();

    // Double tap the minutes.
    await tester.tap(minuteFinder);
    await tester.pump(const Duration(milliseconds: 100));
    await tester.tap(minuteFinder);
    await tester.pumpAndSettle();

    expect(find.byType(TextField), findsWidgets);
  });

  testWidgets('Can not double tap minutes (when not selected) to enter input mode', (WidgetTester tester) async {
    await mediaQueryBoilerplate(tester, false, entryMode: TimePickerEntryMode.dial);
    final Finder minuteFinder = find.ancestor(
      of: find.text('00'),
      matching: find.byType(InkWell),
    );

    expect(find.byType(TextField), findsNothing);

    // Double tap the minutes.
    await tester.tap(minuteFinder);
    await tester.pump(const Duration(milliseconds: 100));
    await tester.tap(minuteFinder);
    await tester.pumpAndSettle();

    expect(find.byType(TextField), findsNothing);
  });

  testWidgets('Entered text returns time', (WidgetTester tester) async {
    late TimeOfDay result;
    await startPicker(tester, (TimeOfDay? time) { result = time!; }, entryMode: TimePickerEntryMode.input);
    await tester.enterText(find.byType(TextField).first, '9');
    await tester.enterText(find.byType(TextField).last, '12');
    await finishPicker(tester);
    expect(result, equals(const TimeOfDay(hour: 9, minute: 12)));
  });

  testWidgets('Toggle to dial mode keeps selected time', (WidgetTester tester) async {
    late TimeOfDay result;
    await startPicker(tester, (TimeOfDay? time) { result = time!; }, entryMode: TimePickerEntryMode.input);
    await tester.enterText(find.byType(TextField).first, '8');
    await tester.enterText(find.byType(TextField).last, '15');
    await tester.tap(find.byIcon(Icons.access_time));
    await finishPicker(tester);
    expect(result, equals(const TimeOfDay(hour: 8, minute: 15)));
  });

  testWidgets('Invalid text prevents dismissing', (WidgetTester tester) async {
    TimeOfDay? result;
    await startPicker(tester, (TimeOfDay? time) { result = time; }, entryMode: TimePickerEntryMode.input);

    // Invalid hour.
    await tester.enterText(find.byType(TextField).first, '88');
    await tester.enterText(find.byType(TextField).last, '15');
    await finishPicker(tester);
    expect(result, null);

    // Invalid minute.
    await tester.enterText(find.byType(TextField).first, '8');
    await tester.enterText(find.byType(TextField).last, '95');
    await finishPicker(tester);
    expect(result, null);

    await tester.enterText(find.byType(TextField).first, '8');
    await tester.enterText(find.byType(TextField).last, '15');
    await finishPicker(tester);
    expect(result, equals(const TimeOfDay(hour: 8, minute: 15)));
  });

  // Fixes regression that was reverted in https://github.com/flutter/flutter/pull/64094#pullrequestreview-469836378.
  testWidgets('Ensure hour/minute fields are top-aligned with the separator', (WidgetTester tester) async {
    await startPicker(tester, (TimeOfDay? time) { }, entryMode: TimePickerEntryMode.input);
    final double hourFieldTop = tester.getTopLeft(find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_HourTextField')).dy;
    final double minuteFieldTop = tester.getTopLeft(find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_MinuteTextField')).dy;
    final double separatorTop = tester.getTopLeft(find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_StringFragment')).dy;
    expect(hourFieldTop, separatorTop);
    expect(minuteFieldTop, separatorTop);
  });

  testWidgets('Time Picker state restoration test - dial mode', (WidgetTester tester) async {
    TimeOfDay? result;
    final Offset center = (await startPicker(
      tester,
      (TimeOfDay? time) { result = time; },
      restorationId: 'restorable_time_picker',
    ))!;
    final Offset hour6 = Offset(center.dx, center.dy + 50.0); // 6:00
    final Offset min45 = Offset(center.dx - 50.0, center.dy); // 45 mins (or 9:00 hours)

    await tester.tapAt(hour6);
    await tester.pump(const Duration(milliseconds: 50));
    await tester.restartAndRestore();
    await tester.tapAt(min45);
    await tester.pump(const Duration(milliseconds: 50));
    final TestRestorationData restorationData = await tester.getRestorationData();
    await tester.restartAndRestore();
    // Setting to PM adds 12 hours (18:45)
    await tester.tap(find.text('PM'));
    await tester.pump(const Duration(milliseconds: 50));
    await tester.restartAndRestore();
    await finishPicker(tester);
    expect(result, equals(const TimeOfDay(hour: 18, minute: 45)));

    // Test restoring from before PM was selected (6:45)
    await tester.restoreFrom(restorationData);
    await finishPicker(tester);
    expect(result, equals(const TimeOfDay(hour: 6, minute: 45)));
  });

  testWidgets('Time Picker state restoration test - input mode', (WidgetTester tester) async {
    TimeOfDay? result;
    await startPicker(
      tester,
      (TimeOfDay? time) { result = time; },
      entryMode: TimePickerEntryMode.input,
      restorationId: 'restorable_time_picker',
    );
    await tester.enterText(find.byType(TextField).first, '9');
    await tester.pump(const Duration(milliseconds: 50));
    await tester.restartAndRestore();

    await tester.enterText(find.byType(TextField).last, '12');
    await tester.pump(const Duration(milliseconds: 50));
    final TestRestorationData restorationData = await tester.getRestorationData();
    await tester.restartAndRestore();

    // Setting to PM adds 12 hours (21:12)
    await tester.tap(find.text('PM'));
    await tester.pump(const Duration(milliseconds: 50));
    await tester.restartAndRestore();

    await finishPicker(tester);
    expect(result, equals(const TimeOfDay(hour: 21, minute: 12)));

    // Restoring from before PM was set (9:12)
    await tester.restoreFrom(restorationData);
    await finishPicker(tester);
    expect(result, equals(const TimeOfDay(hour: 9, minute: 12)));
  });

  testWidgets('Time Picker state restoration test - switching modes', (WidgetTester tester) async {
    TimeOfDay? result;
    final Offset center = (await startPicker(
      tester,
      (TimeOfDay? time) { result = time; },
      restorationId: 'restorable_time_picker',
    ))!;

    final TestRestorationData restorationData = await tester.getRestorationData();
    // Switch to input mode from dial mode.
    await tester.tap(find.byIcon(Icons.keyboard));
    await tester.pump(const Duration(milliseconds: 50));
    await tester.restartAndRestore();

    // Select time using input mode controls.
    await tester.enterText(find.byType(TextField).first, '9');
    await tester.enterText(find.byType(TextField).last, '12');
    await tester.pump(const Duration(milliseconds: 50));
    await finishPicker(tester);
    expect(result, equals(const TimeOfDay(hour: 9, minute: 12)));

    // Restoring from dial mode.
    await tester.restoreFrom(restorationData);
    final Offset hour6 = Offset(center.dx, center.dy + 50.0); // 6:00
    final Offset min45 = Offset(center.dx - 50.0, center.dy); // 45 mins (or 9:00 hours)

    await tester.tapAt(hour6);
    await tester.pump(const Duration(milliseconds: 50));
    await tester.restartAndRestore();
    await tester.tapAt(min45);
    await tester.pump(const Duration(milliseconds: 50));
    await finishPicker(tester);
    expect(result, equals(const TimeOfDay(hour: 6, minute: 45)));
  });
}

final Finder findDialPaint = find.descendant(
  of: find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_Dial'),
  matching: find.byWidgetPredicate((Widget w) => w is CustomPaint),
);

class PickerObserver extends NavigatorObserver {
  int pickerCount = 0;

  @override
  void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) {
    if (route is DialogRoute) {
      pickerCount++;
    }
    super.didPush(route, previousRoute);
  }
}

Future<void> mediaQueryBoilerplate(
  WidgetTester tester,
  bool alwaysUse24HourFormat, {
  TimeOfDay initialTime = const TimeOfDay(hour: 7, minute: 0),
  double textScaleFactor = 1.0,
  TimePickerEntryMode entryMode = TimePickerEntryMode.dial,
  String? helpText,
  String? hourLabelText,
  String? minuteLabelText,
  String? errorInvalidText,
  bool accessibleNavigation = false,
}) async {
  await tester.pumpWidget(
    Localizations(
      locale: const Locale('en', 'US'),
      delegates: const <LocalizationsDelegate<dynamic>>[
        DefaultMaterialLocalizations.delegate,
        DefaultWidgetsLocalizations.delegate,
      ],
      child: MediaQuery(
        data: MediaQueryData(
          alwaysUse24HourFormat: alwaysUse24HourFormat,
          textScaleFactor: textScaleFactor,
          accessibleNavigation: accessibleNavigation,
        ),
        child: Material(
          child: Directionality(
            textDirection: TextDirection.ltr,
            child: Navigator(
              onGenerateRoute: (RouteSettings settings) {
                return MaterialPageRoute<void>(builder: (BuildContext context) {
                  return TextButton(
                    onPressed: () {
                      showTimePicker(
                        context: context,
                        initialTime: initialTime,
                        initialEntryMode: entryMode,
                        helpText: helpText,
                        hourLabelText: hourLabelText,
                        minuteLabelText: minuteLabelText,
                        errorInvalidText: errorInvalidText
                      );
                    },
                    child: const Text('X'),
                  );
                });
              },
            ),
          ),
        ),
      ),
    ),
  );
  await tester.tap(find.text('X'));
  await tester.pumpAndSettle();
}
