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

// TODO(gspencergoog): Remove this tag once this test's state leaks/test
// dependencies have been fixed.
// https://github.com/flutter/flutter/issues/85160
// Fails with "flutter test --test-randomize-ordering-seed=20210721"
@Tags(<String>['no-shuffle'])
library;

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart' hide TextInputAction;
import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart' hide TextInputAction;
import 'package:flutter_driver/flutter_driver.dart';
import 'package:flutter_driver/src/extension/extension.dart';
import 'package:flutter_test/flutter_test.dart' hide TextInputAction;

import 'stubs/stub_command.dart';
import 'stubs/stub_command_extension.dart';
import 'stubs/stub_finder.dart';
import 'stubs/stub_finder_extension.dart';

Future<void> silenceDriverLogger(AsyncCallback callback) async {
  final DriverLogCallback oldLogger = driverLog;
  driverLog = (String source, String message) {};
  try {
    await callback();
  } finally {
    driverLog = oldLogger;
  }
}

void main() {
  group('waitUntilNoTransientCallbacks', () {
    late FlutterDriverExtension driverExtension;
    Map<String, dynamic>? result;
    int messageId = 0;
    final List<String?> log = <String?>[];

    setUp(() {
      result = null;
      driverExtension = FlutterDriverExtension(
        (String? message) async {
          log.add(message);
          return (messageId += 1).toString();
        },
        false,
        true,
      );
    });

    testWidgets('returns immediately when transient callback queue is empty', (
      WidgetTester tester,
    ) async {
      driverExtension
          .call(const WaitForCondition(NoTransientCallbacks()).serialize())
          .then<void>(
            expectAsync1((Map<String, dynamic> r) {
              result = r;
            }),
          );

      await tester.idle();
      expect(result, <String, dynamic>{'isError': false, 'response': <String, dynamic>{}});
    });

    testWidgets('waits until no transient callbacks', (WidgetTester tester) async {
      SchedulerBinding.instance.scheduleFrameCallback((_) {
        // Intentionally blank. We only care about existence of a callback.
      });

      driverExtension
          .call(const WaitForCondition(NoTransientCallbacks()).serialize())
          .then<void>(
            expectAsync1((Map<String, dynamic> r) {
              result = r;
            }),
          );

      // Nothing should happen until the next frame.
      await tester.idle();
      expect(result, isNull);

      // NOW we should receive the result.
      await tester.pump();
      expect(result, <String, dynamic>{'isError': false, 'response': <String, dynamic>{}});
    });

    testWidgets('handler', (WidgetTester tester) async {
      expect(log, isEmpty);
      final Map<String, dynamic> response = await driverExtension.call(
        const RequestData('hello').serialize(),
      );
      final RequestDataResult result = RequestDataResult.fromJson(
        response['response'] as Map<String, dynamic>,
      );
      expect(log, <String>['hello']);
      expect(result.message, '1');
    });
  });

  group('waitForCondition', () {
    late FlutterDriverExtension driverExtension;
    Map<String, dynamic>? result;
    int messageId = 0;
    final List<String?> log = <String?>[];

    setUp(() {
      result = null;
      driverExtension = FlutterDriverExtension(
        (String? message) async {
          log.add(message);
          return (messageId += 1).toString();
        },
        false,
        true,
      );
    });

    testWidgets(
      'waiting for NoTransientCallbacks returns immediately when transient callback queue is empty',
      (WidgetTester tester) async {
        driverExtension
            .call(const WaitForCondition(NoTransientCallbacks()).serialize())
            .then<void>(
              expectAsync1((Map<String, dynamic> r) {
                result = r;
              }),
            );

        await tester.idle();
        expect(result, <String, dynamic>{'isError': false, 'response': <String, dynamic>{}});
      },
    );

    testWidgets('waiting for NoTransientCallbacks returns until no transient callbacks', (
      WidgetTester tester,
    ) async {
      SchedulerBinding.instance.scheduleFrameCallback((_) {
        // Intentionally blank. We only care about existence of a callback.
      });

      driverExtension
          .call(const WaitForCondition(NoTransientCallbacks()).serialize())
          .then<void>(
            expectAsync1((Map<String, dynamic> r) {
              result = r;
            }),
          );

      // Nothing should happen until the next frame.
      await tester.idle();
      expect(result, isNull);

      // NOW we should receive the result.
      await tester.pump();
      expect(result, <String, dynamic>{'isError': false, 'response': <String, dynamic>{}});
    });

    testWidgets('waiting for NoPendingFrame returns immediately when frame is synced', (
      WidgetTester tester,
    ) async {
      driverExtension
          .call(const WaitForCondition(NoPendingFrame()).serialize())
          .then<void>(
            expectAsync1((Map<String, dynamic> r) {
              result = r;
            }),
          );

      await tester.idle();
      expect(result, <String, dynamic>{'isError': false, 'response': <String, dynamic>{}});
    });

    testWidgets('waiting for NoPendingFrame returns until no pending scheduled frame', (
      WidgetTester tester,
    ) async {
      SchedulerBinding.instance.scheduleFrame();

      driverExtension
          .call(const WaitForCondition(NoPendingFrame()).serialize())
          .then<void>(
            expectAsync1((Map<String, dynamic> r) {
              result = r;
            }),
          );

      // Nothing should happen until the next frame.
      await tester.idle();
      expect(result, isNull);

      // NOW we should receive the result.
      await tester.pump();
      expect(result, <String, dynamic>{'isError': false, 'response': <String, dynamic>{}});
    });

    testWidgets('waiting for combined conditions returns immediately', (WidgetTester tester) async {
      const SerializableWaitCondition combinedCondition = CombinedCondition(
        <SerializableWaitCondition>[NoTransientCallbacks(), NoPendingFrame()],
      );
      driverExtension
          .call(const WaitForCondition(combinedCondition).serialize())
          .then<void>(
            expectAsync1((Map<String, dynamic> r) {
              result = r;
            }),
          );

      await tester.idle();
      expect(result, <String, dynamic>{'isError': false, 'response': <String, dynamic>{}});
    });

    testWidgets('waiting for combined conditions returns until no transient callbacks', (
      WidgetTester tester,
    ) async {
      SchedulerBinding.instance.scheduleFrame();
      SchedulerBinding.instance.scheduleFrameCallback((_) {
        // Intentionally blank. We only care about existence of a callback.
      });

      const SerializableWaitCondition combinedCondition = CombinedCondition(
        <SerializableWaitCondition>[NoTransientCallbacks(), NoPendingFrame()],
      );
      driverExtension
          .call(const WaitForCondition(combinedCondition).serialize())
          .then<void>(
            expectAsync1((Map<String, dynamic> r) {
              result = r;
            }),
          );

      // Nothing should happen until the next frame.
      await tester.idle();
      expect(result, isNull);

      // NOW we should receive the result.
      await tester.pump();
      expect(result, <String, dynamic>{'isError': false, 'response': <String, dynamic>{}});
    });

    testWidgets('waiting for combined conditions returns until no pending scheduled frame', (
      WidgetTester tester,
    ) async {
      SchedulerBinding.instance.scheduleFrame();
      SchedulerBinding.instance.scheduleFrameCallback((_) {
        // Intentionally blank. We only care about existence of a callback.
      });

      const SerializableWaitCondition combinedCondition = CombinedCondition(
        <SerializableWaitCondition>[NoPendingFrame(), NoTransientCallbacks()],
      );
      driverExtension
          .call(const WaitForCondition(combinedCondition).serialize())
          .then<void>(
            expectAsync1((Map<String, dynamic> r) {
              result = r;
            }),
          );

      // Nothing should happen until the next frame.
      await tester.idle();
      expect(result, isNull);

      // NOW we should receive the result.
      await tester.pump();
      expect(result, <String, dynamic>{'isError': false, 'response': <String, dynamic>{}});
    });

    testWidgets(
      'waiting for NoPendingPlatformMessages returns immediately when there are no platform messages',
      (WidgetTester tester) async {
        driverExtension
            .call(const WaitForCondition(NoPendingPlatformMessages()).serialize())
            .then<void>(
              expectAsync1((Map<String, dynamic> r) {
                result = r;
              }),
            );

        await tester.idle();
        expect(result, <String, dynamic>{'isError': false, 'response': <String, dynamic>{}});
      },
    );

    testWidgets(
      'waiting for NoPendingPlatformMessages returns until a single method channel call returns',
      (WidgetTester tester) async {
        const MethodChannel channel = MethodChannel('helloChannel', JSONMethodCodec());
        const MessageCodec<dynamic> jsonMessage = JSONMessageCodec();
        tester.binding.defaultBinaryMessenger.setMockMessageHandler('helloChannel', (
          ByteData? message,
        ) {
          return Future<ByteData>.delayed(
            const Duration(milliseconds: 10),
            () => jsonMessage.encodeMessage(<dynamic>['hello world'])!,
          );
        });
        channel.invokeMethod<String>('sayHello', 'hello');

        driverExtension
            .call(const WaitForCondition(NoPendingPlatformMessages()).serialize())
            .then<void>(
              expectAsync1((Map<String, dynamic> r) {
                result = r;
              }),
            );

        // The channel message are delayed for 10 milliseconds, so nothing happens yet.
        await tester.pump(const Duration(milliseconds: 5));
        expect(result, isNull);

        // Now we receive the result.
        await tester.pump(const Duration(milliseconds: 5));
        expect(result, <String, dynamic>{'isError': false, 'response': <String, dynamic>{}});
      },
    );

    testWidgets(
      'waiting for NoPendingPlatformMessages returns until both method channel calls return',
      (WidgetTester tester) async {
        const MessageCodec<dynamic> jsonMessage = JSONMessageCodec();
        // Configures channel 1
        const MethodChannel channel1 = MethodChannel('helloChannel1', JSONMethodCodec());
        tester.binding.defaultBinaryMessenger.setMockMessageHandler('helloChannel1', (
          ByteData? message,
        ) {
          return Future<ByteData>.delayed(
            const Duration(milliseconds: 10),
            () => jsonMessage.encodeMessage(<dynamic>['hello world'])!,
          );
        });

        // Configures channel 2
        const MethodChannel channel2 = MethodChannel('helloChannel2', JSONMethodCodec());
        tester.binding.defaultBinaryMessenger.setMockMessageHandler('helloChannel2', (
          ByteData? message,
        ) {
          return Future<ByteData>.delayed(
            const Duration(milliseconds: 20),
            () => jsonMessage.encodeMessage(<dynamic>['hello world'])!,
          );
        });

        channel1.invokeMethod<String>('sayHello', 'hello');
        channel2.invokeMethod<String>('sayHello', 'hello');

        driverExtension
            .call(const WaitForCondition(NoPendingPlatformMessages()).serialize())
            .then<void>(
              expectAsync1((Map<String, dynamic> r) {
                result = r;
              }),
            );

        // Neither of the channel responses is received, so nothing happens yet.
        await tester.pump(const Duration(milliseconds: 5));
        expect(result, isNull);

        // Result of channel 1 is received, but channel 2 is still pending, so still waiting.
        await tester.pump(const Duration(milliseconds: 10));
        expect(result, isNull);

        // Both of the results are received. Now we receive the result.
        await tester.pump(const Duration(milliseconds: 30));
        expect(result, <String, dynamic>{'isError': false, 'response': <String, dynamic>{}});
      },
    );

    testWidgets(
      'waiting for NoPendingPlatformMessages returns until new method channel call returns',
      (WidgetTester tester) async {
        const MessageCodec<dynamic> jsonMessage = JSONMessageCodec();
        // Configures channel 1
        const MethodChannel channel1 = MethodChannel('helloChannel1', JSONMethodCodec());
        tester.binding.defaultBinaryMessenger.setMockMessageHandler('helloChannel1', (
          ByteData? message,
        ) {
          return Future<ByteData>.delayed(
            const Duration(milliseconds: 10),
            () => jsonMessage.encodeMessage(<dynamic>['hello world'])!,
          );
        });

        // Configures channel 2
        const MethodChannel channel2 = MethodChannel('helloChannel2', JSONMethodCodec());
        tester.binding.defaultBinaryMessenger.setMockMessageHandler('helloChannel2', (
          ByteData? message,
        ) {
          return Future<ByteData>.delayed(
            const Duration(milliseconds: 20),
            () => jsonMessage.encodeMessage(<dynamic>['hello world'])!,
          );
        });

        channel1.invokeMethod<String>('sayHello', 'hello');

        // Calls the waiting API before the second channel message is sent.
        driverExtension
            .call(const WaitForCondition(NoPendingPlatformMessages()).serialize())
            .then<void>(
              expectAsync1((Map<String, dynamic> r) {
                result = r;
              }),
            );

        // The first channel message is not received, so nothing happens yet.
        await tester.pump(const Duration(milliseconds: 5));
        expect(result, isNull);

        channel2.invokeMethod<String>('sayHello', 'hello');

        // Result of channel 1 is received, but channel 2 is still pending, so still waiting.
        await tester.pump(const Duration(milliseconds: 15));
        expect(result, isNull);

        // Both of the results are received. Now we receive the result.
        await tester.pump(const Duration(milliseconds: 10));
        expect(result, <String, dynamic>{'isError': false, 'response': <String, dynamic>{}});
      },
    );

    testWidgets(
      'waiting for NoPendingPlatformMessages returns until both old and new method channel calls return',
      (WidgetTester tester) async {
        const MessageCodec<dynamic> jsonMessage = JSONMessageCodec();
        // Configures channel 1
        const MethodChannel channel1 = MethodChannel('helloChannel1', JSONMethodCodec());
        tester.binding.defaultBinaryMessenger.setMockMessageHandler('helloChannel1', (
          ByteData? message,
        ) {
          return Future<ByteData>.delayed(
            const Duration(milliseconds: 20),
            () => jsonMessage.encodeMessage(<dynamic>['hello world'])!,
          );
        });

        // Configures channel 2
        const MethodChannel channel2 = MethodChannel('helloChannel2', JSONMethodCodec());
        tester.binding.defaultBinaryMessenger.setMockMessageHandler('helloChannel2', (
          ByteData? message,
        ) {
          return Future<ByteData>.delayed(
            const Duration(milliseconds: 10),
            () => jsonMessage.encodeMessage(<dynamic>['hello world'])!,
          );
        });

        channel1.invokeMethod<String>('sayHello', 'hello');

        driverExtension
            .call(const WaitForCondition(NoPendingPlatformMessages()).serialize())
            .then<void>(
              expectAsync1((Map<String, dynamic> r) {
                result = r;
              }),
            );

        // The first channel message is not received, so nothing happens yet.
        await tester.pump(const Duration(milliseconds: 5));
        expect(result, isNull);

        channel2.invokeMethod<String>('sayHello', 'hello');

        // Result of channel 2 is received, but channel 1 is still pending, so still waiting.
        await tester.pump(const Duration(milliseconds: 10));
        expect(result, isNull);

        // Now we receive the result.
        await tester.pump(const Duration(milliseconds: 5));
        expect(result, <String, dynamic>{'isError': false, 'response': <String, dynamic>{}});
      },
    );
  });

  group('getSemanticsId', () {
    late FlutterDriverExtension driverExtension;
    setUp(() {
      driverExtension = FlutterDriverExtension((String? arg) async => '', true, true);
    });

    testWidgets('works when semantics are enabled', (WidgetTester tester) async {
      final SemanticsHandle semantics = tester.ensureSemantics();
      await tester.pumpWidget(const Text('hello', textDirection: TextDirection.ltr));

      final Map<String, String> arguments = GetSemanticsId(const ByText('hello')).serialize();
      final Map<String, dynamic> response = await driverExtension.call(arguments);
      final GetSemanticsIdResult result = GetSemanticsIdResult.fromJson(
        response['response'] as Map<String, dynamic>,
      );

      expect(result.id, 1);
      semantics.dispose();
    });

    testWidgets('throws state error if no data is found', (WidgetTester tester) async {
      await tester.pumpWidget(const Text('hello', textDirection: TextDirection.ltr));

      final Map<String, String> arguments = GetSemanticsId(const ByText('hello')).serialize();
      final Map<String, dynamic> response = await driverExtension.call(arguments);

      expect(response['isError'], true);
      expect(response['response'], contains('Bad state: No semantics data found'));
    }, semanticsEnabled: false);

    testWidgets('throws state error multiple matches are found', (WidgetTester tester) async {
      final SemanticsHandle semantics = tester.ensureSemantics();
      await tester.pumpWidget(
        Directionality(
          textDirection: TextDirection.ltr,
          child: ListView(
            children: const <Widget>[
              SizedBox(width: 100.0, height: 100.0, child: Text('hello')),
              SizedBox(width: 100.0, height: 100.0, child: Text('hello')),
            ],
          ),
        ),
      );

      final Map<String, String> arguments = GetSemanticsId(const ByText('hello')).serialize();
      final Map<String, dynamic> response = await driverExtension.call(arguments);

      expect(response['isError'], true);
      expect(
        response['response'],
        contains('Bad state: Found more than one element with the same ID'),
      );
      semantics.dispose();
    });
  });

  testWidgets('getOffset', (WidgetTester tester) async {
    final FlutterDriverExtension driverExtension = FlutterDriverExtension(
      (String? arg) async => '',
      true,
      true,
    );

    Future<Offset> getOffset(OffsetType offset) async {
      final Map<String, String> arguments = GetOffset(ByValueKey(1), offset).serialize();
      final Map<String, dynamic> response = await driverExtension.call(arguments);
      final GetOffsetResult result = GetOffsetResult.fromJson(
        response['response'] as Map<String, dynamic>,
      );
      return Offset(result.dx, result.dy);
    }

    await tester.pumpWidget(
      Align(
        alignment: Alignment.topLeft,
        child: Transform.translate(
          offset: const Offset(40, 30),
          child: const SizedBox(key: ValueKey<int>(1), width: 100, height: 120),
        ),
      ),
    );

    expect(await getOffset(OffsetType.topLeft), const Offset(40, 30));
    expect(await getOffset(OffsetType.topRight), const Offset(40 + 100.0, 30));
    expect(await getOffset(OffsetType.bottomLeft), const Offset(40, 30 + 120.0));
    expect(await getOffset(OffsetType.bottomRight), const Offset(40 + 100.0, 30 + 120.0));
    expect(await getOffset(OffsetType.center), const Offset(40 + (100 / 2), 30 + (120 / 2)));
  });

  testWidgets('getText', (WidgetTester tester) async {
    await silenceDriverLogger(() async {
      final FlutterDriverExtension driverExtension = FlutterDriverExtension(
        (String? arg) async => '',
        true,
        true,
      );

      Future<String?> getTextInternal(SerializableFinder search) async {
        final Map<String, String> arguments = GetText(
          search,
          timeout: const Duration(seconds: 1),
        ).serialize();
        final Map<String, dynamic> result = await driverExtension.call(arguments);
        if (result['isError'] as bool) {
          return null;
        }
        return GetTextResult.fromJson(result['response'] as Map<String, dynamic>).text;
      }

      await tester.pumpWidget(
        MaterialApp(
          home: Scaffold(
            body: Column(
              key: const ValueKey<String>('column'),
              children: <Widget>[
                const Text('Hello1', key: ValueKey<String>('text1')),
                SizedBox(
                  height: 25.0,
                  child: RichText(
                    key: const ValueKey<String>('text2'),
                    text: const TextSpan(text: 'Hello2'),
                  ),
                ),
                SizedBox(
                  height: 25.0,
                  child: EditableText(
                    key: const ValueKey<String>('text3'),
                    controller: TextEditingController(text: 'Hello3'),
                    focusNode: FocusNode(),
                    style: const TextStyle(),
                    cursorColor: Colors.red,
                    backgroundCursorColor: Colors.black,
                  ),
                ),
                SizedBox(
                  height: 25.0,
                  child: TextField(
                    key: const ValueKey<String>('text4'),
                    controller: TextEditingController(text: 'Hello4'),
                  ),
                ),
                SizedBox(
                  height: 25.0,
                  child: TextFormField(
                    key: const ValueKey<String>('text5'),
                    controller: TextEditingController(text: 'Hello5'),
                  ),
                ),
                SizedBox(
                  height: 25.0,
                  child: RichText(
                    key: const ValueKey<String>('text6'),
                    text: const TextSpan(
                      children: <TextSpan>[
                        TextSpan(text: 'Hello'),
                        TextSpan(text: ', '),
                        TextSpan(text: 'World'),
                        TextSpan(text: '!'),
                      ],
                    ),
                  ),
                ),
              ],
            ),
          ),
        ),
      );

      expect(await getTextInternal(ByValueKey('text1')), 'Hello1');
      expect(await getTextInternal(ByValueKey('text2')), 'Hello2');
      expect(await getTextInternal(ByValueKey('text3')), 'Hello3');
      expect(await getTextInternal(ByValueKey('text4')), 'Hello4');
      expect(await getTextInternal(ByValueKey('text5')), 'Hello5');
      expect(await getTextInternal(ByValueKey('text6')), 'Hello, World!');

      // Check if error thrown for other types
      final Map<String, String> arguments = GetText(
        ByValueKey('column'),
        timeout: const Duration(seconds: 1),
      ).serialize();
      final Map<String, dynamic> response = await driverExtension.call(arguments);
      expect(response['isError'], true);
      expect(response['response'], contains('is currently not supported by getText'));
    });
  });

  testWidgets('descendant finder', (WidgetTester tester) async {
    await silenceDriverLogger(() async {
      final FlutterDriverExtension driverExtension = FlutterDriverExtension(
        (String? arg) async => '',
        true,
        true,
      );

      Future<String?> getDescendantText({String? of, bool matchRoot = false}) async {
        final Map<String, String> arguments = GetText(
          Descendant(of: ByValueKey(of), matching: ByValueKey('text2'), matchRoot: matchRoot),
          timeout: const Duration(seconds: 1),
        ).serialize();
        final Map<String, dynamic> result = await driverExtension.call(arguments);
        if (result['isError'] as bool) {
          return null;
        }
        return GetTextResult.fromJson(result['response'] as Map<String, dynamic>).text;
      }

      await tester.pumpWidget(
        const MaterialApp(
          home: Column(
            key: ValueKey<String>('column'),
            children: <Widget>[
              Text('Hello1', key: ValueKey<String>('text1')),
              Text('Hello2', key: ValueKey<String>('text2')),
              Text('Hello3', key: ValueKey<String>('text3')),
            ],
          ),
        ),
      );

      expect(await getDescendantText(of: 'column'), 'Hello2');
      expect(await getDescendantText(of: 'column', matchRoot: true), 'Hello2');
      expect(await getDescendantText(of: 'text2', matchRoot: true), 'Hello2');

      // Find nothing
      Future<String?> result = getDescendantText(of: 'text1', matchRoot: true);
      await tester.pump(const Duration(seconds: 2));
      expect(await result, null);

      result = getDescendantText(of: 'text2');
      await tester.pump(const Duration(seconds: 2));
      expect(await result, null);
    });
  });

  testWidgets('descendant finder firstMatchOnly', (WidgetTester tester) async {
    await silenceDriverLogger(() async {
      final FlutterDriverExtension driverExtension = FlutterDriverExtension(
        (String? arg) async => '',
        true,
        true,
      );

      Future<String?> getDescendantText() async {
        final Map<String, String> arguments = GetText(
          Descendant(
            of: ByValueKey('column'),
            matching: const ByType('Text'),
            firstMatchOnly: true,
          ),
          timeout: const Duration(seconds: 1),
        ).serialize();
        final Map<String, dynamic> result = await driverExtension.call(arguments);
        if (result['isError'] as bool) {
          return null;
        }
        return GetTextResult.fromJson(result['response'] as Map<String, dynamic>).text;
      }

      await tester.pumpWidget(
        const MaterialApp(
          home: Column(
            key: ValueKey<String>('column'),
            children: <Widget>[
              Text('Hello1', key: ValueKey<String>('text1')),
              Text('Hello2', key: ValueKey<String>('text2')),
              Text('Hello3', key: ValueKey<String>('text3')),
            ],
          ),
        ),
      );

      expect(await getDescendantText(), 'Hello1');
    });
  });

  testWidgets('ancestor finder', (WidgetTester tester) async {
    await silenceDriverLogger(() async {
      final FlutterDriverExtension driverExtension = FlutterDriverExtension(
        (String? arg) async => '',
        true,
        true,
      );

      Future<Offset?> getAncestorTopLeft({
        String? of,
        String? matching,
        bool matchRoot = false,
      }) async {
        final Map<String, String> arguments = GetOffset(
          Ancestor(of: ByValueKey(of), matching: ByValueKey(matching), matchRoot: matchRoot),
          OffsetType.topLeft,
          timeout: const Duration(seconds: 1),
        ).serialize();
        final Map<String, dynamic> response = await driverExtension.call(arguments);
        if (response['isError'] as bool) {
          return null;
        }
        final GetOffsetResult result = GetOffsetResult.fromJson(
          response['response'] as Map<String, dynamic>,
        );
        return Offset(result.dx, result.dy);
      }

      await tester.pumpWidget(
        const MaterialApp(
          home: Center(
            child: SizedBox(
              key: ValueKey<String>('parent'),
              height: 100,
              width: 100,
              child: Center(
                child: Row(
                  children: <Widget>[
                    SizedBox(key: ValueKey<String>('leftchild'), width: 25, height: 25),
                    SizedBox(key: ValueKey<String>('rightchild'), width: 25, height: 25),
                  ],
                ),
              ),
            ),
          ),
        ),
      );

      expect(
        await getAncestorTopLeft(of: 'leftchild', matching: 'parent'),
        const Offset((800 - 100) / 2, (600 - 100) / 2),
      );
      expect(
        await getAncestorTopLeft(of: 'leftchild', matching: 'parent', matchRoot: true),
        const Offset((800 - 100) / 2, (600 - 100) / 2),
      );
      expect(
        await getAncestorTopLeft(of: 'parent', matching: 'parent', matchRoot: true),
        const Offset((800 - 100) / 2, (600 - 100) / 2),
      );

      // Find nothing
      Future<Offset?> result = getAncestorTopLeft(of: 'leftchild', matching: 'leftchild');
      await tester.pump(const Duration(seconds: 2));
      expect(await result, null);

      result = getAncestorTopLeft(of: 'leftchild', matching: 'rightchild');
      await tester.pump(const Duration(seconds: 2));
      expect(await result, null);
    });
  });

  testWidgets('ancestor finder firstMatchOnly', (WidgetTester tester) async {
    await silenceDriverLogger(() async {
      final FlutterDriverExtension driverExtension = FlutterDriverExtension(
        (String? arg) async => '',
        true,
        true,
      );

      Future<Offset?> getAncestorTopLeft() async {
        final Map<String, String> arguments = GetOffset(
          Ancestor(
            of: ByValueKey('leaf'),
            matching: const ByType('SizedBox'),
            firstMatchOnly: true,
          ),
          OffsetType.topLeft,
          timeout: const Duration(seconds: 1),
        ).serialize();
        final Map<String, dynamic> response = await driverExtension.call(arguments);
        if (response['isError'] as bool) {
          return null;
        }
        final GetOffsetResult result = GetOffsetResult.fromJson(
          response['response'] as Map<String, dynamic>,
        );
        return Offset(result.dx, result.dy);
      }

      await tester.pumpWidget(
        const MaterialApp(
          home: Center(
            child: SizedBox(
              height: 200,
              width: 200,
              child: Center(
                child: SizedBox(
                  height: 100,
                  width: 100,
                  child: Center(
                    child: SizedBox(key: ValueKey<String>('leaf'), height: 50, width: 50),
                  ),
                ),
              ),
            ),
          ),
        ),
      );

      expect(await getAncestorTopLeft(), const Offset((800 - 100) / 2, (600 - 100) / 2));
    });
  });

  testWidgets('GetDiagnosticsTree', (WidgetTester tester) async {
    final FlutterDriverExtension driverExtension = FlutterDriverExtension(
      (String? arg) async => '',
      true,
      true,
    );

    Future<Map<String, dynamic>> getDiagnosticsTree(
      DiagnosticsType type,
      SerializableFinder finder, {
      int depth = 0,
      bool properties = true,
    }) async {
      final Map<String, String> arguments = GetDiagnosticsTree(
        finder,
        type,
        subtreeDepth: depth,
        includeProperties: properties,
      ).serialize();
      final Map<String, dynamic> response = await driverExtension.call(arguments);
      final DiagnosticsTreeResult result = DiagnosticsTreeResult(
        response['response'] as Map<String, dynamic>,
      );
      return result.json;
    }

    await tester.pumpWidget(
      const Directionality(
        textDirection: TextDirection.ltr,
        child: Center(child: Text('Hello World', key: ValueKey<String>('Text'))),
      ),
    );

    // Widget
    Map<String, dynamic> result = await getDiagnosticsTree(
      DiagnosticsType.widget,
      ByValueKey('Text'),
    );
    expect(result['children'], isNull); // depth: 0
    expect(result['widgetRuntimeType'], 'Text');

    List<Map<String, dynamic>> properties = (result['properties']! as List<Object>)
        .cast<Map<String, dynamic>>();
    Map<String, dynamic> stringProperty = properties.singleWhere(
      (Map<String, dynamic> property) => property['name'] == 'data',
    );
    expect(stringProperty['description'], '"Hello World"');
    expect(stringProperty['propertyType'], 'String');

    result = await getDiagnosticsTree(
      DiagnosticsType.widget,
      ByValueKey('Text'),
      properties: false,
    );
    expect(result['widgetRuntimeType'], 'Text');
    expect(result['properties'], isNull); // properties: false

    result = await getDiagnosticsTree(DiagnosticsType.widget, ByValueKey('Text'), depth: 1);
    List<Map<String, dynamic>> children = (result['children']! as List<Object>)
        .cast<Map<String, dynamic>>();
    expect(children.single['children'], isNull);

    result = await getDiagnosticsTree(DiagnosticsType.widget, ByValueKey('Text'), depth: 100);
    children = (result['children']! as List<Object>).cast<Map<String, dynamic>>();
    expect(children.single['children'], isEmpty);

    // RenderObject
    result = await getDiagnosticsTree(DiagnosticsType.renderObject, ByValueKey('Text'));
    expect(result['children'], isNull); // depth: 0
    expect(result['properties'], isNotNull);
    expect(result['description'], startsWith('RenderParagraph'));

    result = await getDiagnosticsTree(
      DiagnosticsType.renderObject,
      ByValueKey('Text'),
      properties: false,
    );
    expect(result['properties'], isNull); // properties: false
    expect(result['description'], startsWith('RenderParagraph'));

    result = await getDiagnosticsTree(DiagnosticsType.renderObject, ByValueKey('Text'), depth: 1);
    children = (result['children']! as List<Object>).cast<Map<String, dynamic>>();
    final Map<String, dynamic> textSpan = children.single;
    expect(textSpan['description'], 'TextSpan');
    properties = (textSpan['properties']! as List<Object>).cast<Map<String, dynamic>>();
    stringProperty = properties.singleWhere(
      (Map<String, dynamic> property) => property['name'] == 'text',
    );
    expect(stringProperty['description'], '"Hello World"');
    expect(stringProperty['propertyType'], 'String');
    expect(children.single['children'], isNull);

    result = await getDiagnosticsTree(DiagnosticsType.renderObject, ByValueKey('Text'), depth: 100);
    children = (result['children']! as List<Object>).cast<Map<String, dynamic>>();
    expect(children.single['children'], isEmpty);
  });

  group('enableTextEntryEmulation', () {
    late FlutterDriverExtension driverExtension;

    Future<Map<String, dynamic>> enterText() async {
      final Map<String, String> arguments = const EnterText('foo').serialize();
      final Map<String, dynamic> result = await driverExtension.call(arguments);
      return result;
    }

    const Widget testWidget = MaterialApp(
      home: Material(
        child: Center(child: TextField(key: ValueKey<String>('foo'), autofocus: true)),
      ),
    );

    testWidgets('enableTextEntryEmulation false', (WidgetTester tester) async {
      driverExtension = FlutterDriverExtension((String? arg) async => '', true, false);

      await tester.pumpWidget(testWidget);

      final Map<String, dynamic> enterTextResult = await enterText();
      expect(enterTextResult['isError'], isTrue);
    });

    testWidgets('enableTextEntryEmulation true', (WidgetTester tester) async {
      driverExtension = FlutterDriverExtension((String? arg) async => '', true, true);

      await tester.pumpWidget(testWidget);

      final Map<String, dynamic> enterTextResult = await enterText();
      expect(enterTextResult['isError'], isFalse);
    });
  });

  group('extension finders', () {
    final Widget debugTree = Directionality(
      textDirection: TextDirection.ltr,
      child: Center(
        child: Column(
          key: const ValueKey<String>('Column'),
          children: <Widget>[
            const Text('Foo', key: ValueKey<String>('Text1')),
            const Text('Bar', key: ValueKey<String>('Text2')),
            TextButton(
              key: const ValueKey<String>('Button'),
              onPressed: () {},
              child: const Text('Whatever'),
            ),
          ],
        ),
      ),
    );

    testWidgets('unknown extension finder', (WidgetTester tester) async {
      final FlutterDriverExtension driverExtension = FlutterDriverExtension(
        (String? arg) async => '',
        true,
        true,
        finders: <FinderExtension>[],
      );

      Future<Map<String, dynamic>> getText(SerializableFinder finder) async {
        final Map<String, String> arguments = GetText(
          finder,
          timeout: const Duration(seconds: 1),
        ).serialize();
        return driverExtension.call(arguments);
      }

      await tester.pumpWidget(debugTree);

      final Map<String, dynamic> result = await getText(StubFinder('Text1'));
      expect(result['isError'], true);
      expect(result['response'] is String, true);
      expect(result['response'] as String?, contains('Unsupported search specification type Stub'));
    });

    testWidgets('simple extension finder', (WidgetTester tester) async {
      final FlutterDriverExtension driverExtension = FlutterDriverExtension(
        (String? arg) async => '',
        true,
        true,
        finders: <FinderExtension>[StubFinderExtension()],
      );

      Future<GetTextResult> getText(SerializableFinder finder) async {
        final Map<String, String> arguments = GetText(
          finder,
          timeout: const Duration(seconds: 1),
        ).serialize();
        final Map<String, dynamic> response = await driverExtension.call(arguments);
        return GetTextResult.fromJson(response['response'] as Map<String, dynamic>);
      }

      await tester.pumpWidget(debugTree);

      final GetTextResult result = await getText(StubFinder('Text1'));
      expect(result.text, 'Foo');
    });

    testWidgets('complex extension finder', (WidgetTester tester) async {
      final FlutterDriverExtension driverExtension = FlutterDriverExtension(
        (String? arg) async => '',
        true,
        true,
        finders: <FinderExtension>[StubFinderExtension()],
      );

      Future<GetTextResult> getText(SerializableFinder finder) async {
        final Map<String, String> arguments = GetText(
          finder,
          timeout: const Duration(seconds: 1),
        ).serialize();
        final Map<String, dynamic> response = await driverExtension.call(arguments);
        return GetTextResult.fromJson(response['response'] as Map<String, dynamic>);
      }

      await tester.pumpWidget(debugTree);

      final GetTextResult result = await getText(
        Descendant(of: StubFinder('Column'), matching: StubFinder('Text1')),
      );
      expect(result.text, 'Foo');
    });

    testWidgets('extension finder with command', (WidgetTester tester) async {
      final FlutterDriverExtension driverExtension = FlutterDriverExtension(
        (String? arg) async => '',
        true,
        true,
        finders: <FinderExtension>[StubFinderExtension()],
      );

      Future<Map<String, dynamic>> tap(SerializableFinder finder) async {
        final Map<String, String> arguments = Tap(
          finder,
          timeout: const Duration(seconds: 1),
        ).serialize();
        return driverExtension.call(arguments);
      }

      await tester.pumpWidget(debugTree);

      final Map<String, dynamic> result = await tap(StubFinder('Button'));
      expect(result['isError'], false);
    });
  });

  group('extension commands', () {
    int invokes = 0;
    void stubCallback() => invokes++;

    final Widget debugTree = Directionality(
      textDirection: TextDirection.ltr,
      child: Center(
        child: Column(
          children: <Widget>[
            TextButton(
              key: const ValueKey<String>('Button'),
              onPressed: stubCallback,
              child: const Text('Whatever'),
            ),
          ],
        ),
      ),
    );

    setUp(() {
      invokes = 0;
    });

    testWidgets('unknown extension command', (WidgetTester tester) async {
      final FlutterDriverExtension driverExtension = FlutterDriverExtension(
        (String? arg) async => '',
        true,
        true,
        commands: <CommandExtension>[],
      );

      Future<Map<String, dynamic>> invokeCommand(SerializableFinder finder, int times) async {
        final Map<String, String> arguments = StubNestedCommand(finder, times).serialize();
        return driverExtension.call(arguments);
      }

      await tester.pumpWidget(debugTree);

      final Map<String, dynamic> result = await invokeCommand(ByValueKey('Button'), 10);
      expect(result['isError'], true);
      expect(result['response'] is String, true);
      expect(result['response'] as String?, contains('Unsupported command kind StubNestedCommand'));
    });

    testWidgets('nested command', (WidgetTester tester) async {
      final FlutterDriverExtension driverExtension = FlutterDriverExtension(
        (String? arg) async => '',
        true,
        true,
        commands: <CommandExtension>[StubNestedCommandExtension()],
      );

      Future<StubCommandResult> invokeCommand(SerializableFinder finder, int times) async {
        await driverExtension.call(
          const SetFrameSync(false).serialize(),
        ); // disable frame sync for test to avoid lock
        final Map<String, String> arguments = StubNestedCommand(
          finder,
          times,
          timeout: const Duration(seconds: 1),
        ).serialize();
        final Map<String, dynamic> response = await driverExtension.call(arguments);
        final Map<String, dynamic> commandResponse = response['response'] as Map<String, dynamic>;
        return StubCommandResult(commandResponse['resultParam'] as String);
      }

      await tester.pumpWidget(debugTree);

      const int times = 10;
      final StubCommandResult result = await invokeCommand(ByValueKey('Button'), times);
      expect(result.resultParam, 'stub response');
      expect(invokes, times);
    });

    testWidgets('prober command', (WidgetTester tester) async {
      final FlutterDriverExtension driverExtension = FlutterDriverExtension(
        (String? arg) async => '',
        true,
        true,
        commands: <CommandExtension>[StubProberCommandExtension()],
      );

      Future<StubCommandResult> invokeCommand(SerializableFinder finder, int times) async {
        await driverExtension.call(
          const SetFrameSync(false).serialize(),
        ); // disable frame sync for test to avoid lock
        final Map<String, String> arguments = StubProberCommand(
          finder,
          times,
          timeout: const Duration(seconds: 1),
        ).serialize();
        final Map<String, dynamic> response = await driverExtension.call(arguments);
        final Map<String, dynamic> commandResponse = response['response'] as Map<String, dynamic>;
        return StubCommandResult(commandResponse['resultParam'] as String);
      }

      await tester.pumpWidget(debugTree);

      const int times = 10;
      final StubCommandResult result = await invokeCommand(ByValueKey('Button'), times);
      expect(result.resultParam, 'stub response');
      expect(invokes, times);
    });
  });

  group('waitForTappable', () {
    late FlutterDriverExtension driverExtension;

    Future<Map<String, dynamic>> waitForTappable() async {
      final SerializableFinder finder = ByValueKey('widgetOne');
      final Map<String, String> arguments = WaitForTappable(finder).serialize();
      final Map<String, dynamic> result = await driverExtension.call(arguments);
      return result;
    }

    const Widget testWidget = MaterialApp(
      home: Material(
        child: Column(
          children: <Widget>[
            Text('Hello ', key: Key('widgetOne')),
            SizedBox.shrink(child: Text('World!', key: Key('widgetTwo'))),
          ],
        ),
      ),
    );

    testWidgets('returns true when widget is tappable', (WidgetTester tester) async {
      driverExtension = FlutterDriverExtension((String? arg) async => '', true, false);

      await tester.pumpWidget(testWidget);

      final Map<String, dynamic> waitForTappableResult = await waitForTappable();
      expect(waitForTappableResult['isError'], isFalse);
    });
  });

  group('waitUntilFrameSync', () {
    late FlutterDriverExtension driverExtension;
    Map<String, dynamic>? result;

    setUp(() {
      driverExtension = FlutterDriverExtension((String? arg) async => '', true, true);
      result = null;
    });

    testWidgets('returns immediately when frame is synced', (WidgetTester tester) async {
      driverExtension
          .call(const WaitForCondition(NoPendingFrame()).serialize())
          .then<void>(
            expectAsync1((Map<String, dynamic> r) {
              result = r;
            }),
          );

      await tester.idle();
      expect(result, <String, dynamic>{'isError': false, 'response': <String, dynamic>{}});
    });

    testWidgets('waits until no transient callbacks', (WidgetTester tester) async {
      SchedulerBinding.instance.scheduleFrameCallback((_) {
        // Intentionally blank. We only care about existence of a callback.
      });

      driverExtension
          .call(const WaitForCondition(NoPendingFrame()).serialize())
          .then<void>(
            expectAsync1((Map<String, dynamic> r) {
              result = r;
            }),
          );

      // Nothing should happen until the next frame.
      await tester.idle();
      expect(result, isNull);

      // NOW we should receive the result.
      await tester.pump();
      expect(result, <String, dynamic>{'isError': false, 'response': <String, dynamic>{}});
    });

    testWidgets('waits until no pending scheduled frame', (WidgetTester tester) async {
      SchedulerBinding.instance.scheduleFrame();

      driverExtension
          .call(const WaitForCondition(NoPendingFrame()).serialize())
          .then<void>(
            expectAsync1((Map<String, dynamic> r) {
              result = r;
            }),
          );

      // Nothing should happen until the next frame.
      await tester.idle();
      expect(result, isNull);

      // NOW we should receive the result.
      await tester.pump();
      expect(result, <String, dynamic>{'isError': false, 'response': <String, dynamic>{}});
    });
  });

  group('sendTextInputAction', () {
    late FlutterDriverExtension driverExtension;

    Future<void> sendAction(TextInputAction action) async {
      final Map<String, String> arguments = SendTextInputAction(action).serialize();
      await driverExtension.call(arguments);
    }

    MaterialApp testWidget(TextEditingController controller) => MaterialApp(
      home: Material(
        child: Center(
          child: TextField(
            key: const ValueKey<String>('foo'),
            autofocus: true,
            controller: controller,
            onSubmitted: (_) {
              controller.value = const TextEditingValue(text: 'bar');
            },
          ),
        ),
      ),
    );

    testWidgets('press done trigger onSubmitted and change value', (WidgetTester tester) async {
      driverExtension = FlutterDriverExtension((String? arg) async => '', true, true);

      final TextEditingController controller = TextEditingController(text: 'foo');
      await tester.pumpWidget(testWidget(controller));

      expect(controller.value.text, 'foo');
      await sendAction(TextInputAction.done);
      expectSync(controller.value.text, 'bar');
    });
  });
}
