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

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

import 'keyboard_utils.dart';

void main() {
  Widget buildSpyAboveEditableText({
    required FocusNode editableFocusNode,
    required FocusNode spyFocusNode,
  }) {
    final TextEditingController controller = TextEditingController(text: 'dummy text');
    addTearDown(controller.dispose);

    return MaterialApp(
      home: Align(
        alignment: Alignment.topLeft,
        child: SizedBox(
          // Softwrap at exactly 20 characters.
          width: 201,
          height: 200,
          child: ActionSpy(
            focusNode: spyFocusNode,
            child: EditableText(
              controller: controller,
              showSelectionHandles: true,
              autofocus: true,
              focusNode: editableFocusNode,
              style: const TextStyle(fontSize: 10.0),
              textScaleFactor: 1,
              // Avoid the cursor from taking up width.
              cursorWidth: 0,
              cursorColor: Colors.blue,
              backgroundCursorColor: Colors.grey,
              selectionControls: materialTextSelectionControls,
              keyboardType: TextInputType.text,
              maxLines: null,
              textAlign: TextAlign.left,
            ),
          ),
        ),
      ),
    );
  }

  group('iOS: do not handle delete/backspace events', () {
    final TargetPlatformVariant iOS = TargetPlatformVariant.only(TargetPlatform.iOS);
    final FocusNode editable = FocusNode();
    final FocusNode spy = FocusNode();

    testWidgets('backspace with and without word modifier', (WidgetTester tester) async {
      tester.binding.testTextInput.unregister();
      addTearDown(tester.binding.testTextInput.register);

      await tester.pumpWidget(
        buildSpyAboveEditableText(
          editableFocusNode: editable,
          spyFocusNode: spy,
        ),
      );
      editable.requestFocus();
      await tester.pump();
      final ActionSpyState state = tester.state<ActionSpyState>(find.byType(ActionSpy));

      for (int altShiftState = 0; altShiftState < 1 << 2; altShiftState += 1) {
        final bool alt = altShiftState & 0x1 != 0;
        final bool shift = altShiftState & 0x2 != 0;
        await sendKeyCombination(tester, SingleActivator(LogicalKeyboardKey.backspace, alt: alt, shift: shift));
      }
      await tester.pump();

      expect(state.lastIntent, isNull);
    }, variant: iOS);

    testWidgets('delete with and without word modifier', (WidgetTester tester) async {
      tester.binding.testTextInput.unregister();
      addTearDown(tester.binding.testTextInput.register);

      await tester.pumpWidget(
        buildSpyAboveEditableText(
          editableFocusNode: editable,
          spyFocusNode: spy,
        ),
      );
      editable.requestFocus();
      await tester.pump();
      final ActionSpyState state = tester.state<ActionSpyState>(find.byType(ActionSpy));

      for (int altShiftState = 0; altShiftState < 1 << 2; altShiftState += 1) {
        final bool alt = altShiftState & 0x1 != 0;
        final bool shift = altShiftState & 0x2 != 0;
        await sendKeyCombination(tester, SingleActivator(LogicalKeyboardKey.delete, alt: alt, shift: shift));
      }
      await tester.pump();

      expect(state.lastIntent, isNull);
    }, variant: iOS);

    testWidgets('Exception: deleting to line boundary is handled by the framework', (WidgetTester tester) async {
      tester.binding.testTextInput.unregister();
      addTearDown(tester.binding.testTextInput.register);

      await tester.pumpWidget(
        buildSpyAboveEditableText(
          editableFocusNode: editable,
          spyFocusNode: spy,
        ),
      );
      editable.requestFocus();
      await tester.pump();
      final ActionSpyState state = tester.state<ActionSpyState>(find.byType(ActionSpy));

      for (int keyState = 0; keyState < 1 << 2; keyState += 1) {
        final bool shift = keyState & 0x1 != 0;
        final LogicalKeyboardKey key = keyState & 0x2 != 0 ? LogicalKeyboardKey.delete : LogicalKeyboardKey.backspace;

        state.lastIntent = null;
        final SingleActivator activator = SingleActivator(key, meta: true, shift: shift);
        await sendKeyCombination(tester, activator);
        await tester.pump();
        expect(state.lastIntent, isA<DeleteToLineBreakIntent>(), reason: '$activator');
      }
    }, variant: iOS);
  }, skip: kIsWeb); // [intended] specific tests target non-web.

  group('macOS does not accept shortcuts if focus under EditableText', () {
    final TargetPlatformVariant macOSOnly = TargetPlatformVariant.only(TargetPlatform.macOS);

    testWidgets('word modifier + arrowLeft', (WidgetTester tester) async {
      tester.binding.testTextInput.unregister();
      addTearDown((){
        tester.binding.testTextInput.register();
      });
      final FocusNode editable = FocusNode();
      addTearDown(editable.dispose);
      final FocusNode spy = FocusNode();
      addTearDown(spy.dispose);
      await tester.pumpWidget(
        buildSpyAboveEditableText(
          editableFocusNode: editable,
          spyFocusNode: spy,
        ),
      );
      editable.requestFocus();
      await tester.pump();
      final ActionSpyState state = tester.state<ActionSpyState>(find.byType(ActionSpy));

      await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.arrowLeft, alt: true));
      await tester.pump();

      expect(state.lastIntent, isNull);
    }, variant: macOSOnly);

    testWidgets('word modifier + arrowRight', (WidgetTester tester) async {
      tester.binding.testTextInput.unregister();
      addTearDown((){
        tester.binding.testTextInput.register();
      });
      final FocusNode editable = FocusNode();
      addTearDown(editable.dispose);
      final FocusNode spy = FocusNode();
      addTearDown(spy.dispose);
      await tester.pumpWidget(
        buildSpyAboveEditableText(
          editableFocusNode: editable,
          spyFocusNode: spy,
        ),
      );
      editable.requestFocus();
      await tester.pump();
      final ActionSpyState state = tester.state<ActionSpyState>(find.byType(ActionSpy));

      await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.arrowRight, alt: true));
      await tester.pump();

      expect(state.lastIntent, isNull);
    }, variant: macOSOnly);

    testWidgets('line modifier + arrowLeft', (WidgetTester tester) async {
      tester.binding.testTextInput.unregister();
      addTearDown((){
        tester.binding.testTextInput.register();
      });
      final FocusNode editable = FocusNode();
      addTearDown(editable.dispose);
      final FocusNode spy = FocusNode();
      addTearDown(spy.dispose);
      await tester.pumpWidget(
        buildSpyAboveEditableText(
          editableFocusNode: editable,
          spyFocusNode: spy,
        ),
      );
      editable.requestFocus();
      await tester.pump();
      final ActionSpyState state = tester.state<ActionSpyState>(find.byType(ActionSpy));

      await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.arrowLeft, meta: true));
      await tester.pump();

      expect(state.lastIntent, isNull);
    }, variant: macOSOnly);

    testWidgets('line modifier + arrowRight', (WidgetTester tester) async {
      tester.binding.testTextInput.unregister();
      addTearDown((){
        tester.binding.testTextInput.register();
      });
      final FocusNode editable = FocusNode();
      addTearDown(editable.dispose);
      final FocusNode spy = FocusNode();
      addTearDown(spy.dispose);
      await tester.pumpWidget(
        buildSpyAboveEditableText(
          editableFocusNode: editable,
          spyFocusNode: spy,
        ),
      );
      editable.requestFocus();
      await tester.pump();
      final ActionSpyState state = tester.state<ActionSpyState>(find.byType(ActionSpy));

      await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.arrowRight, meta: true));
      await tester.pump();

      expect(state.lastIntent, isNull);
    }, variant: macOSOnly);

    testWidgets('word modifier + arrow key movement', (WidgetTester tester) async {
      tester.binding.testTextInput.unregister();
      addTearDown((){
        tester.binding.testTextInput.register();
      });
      final FocusNode editable = FocusNode();
      addTearDown(editable.dispose);
      final FocusNode spy = FocusNode();
      addTearDown(spy.dispose);
      await tester.pumpWidget(
        buildSpyAboveEditableText(
          editableFocusNode: editable,
          spyFocusNode: spy,
        ),
      );
      editable.requestFocus();
      await tester.pump();
      final ActionSpyState state = tester.state<ActionSpyState>(find.byType(ActionSpy));

      await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.arrowLeft, alt: true));
      await tester.pump();

      expect(state.lastIntent, isNull);

      await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.arrowLeft, alt: true));
      await tester.pump();
      expect(state.lastIntent, isNull);

      await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.arrowRight, alt: true));
      await tester.pump();
      expect(state.lastIntent, isNull);

      await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.arrowRight, alt: true));
      await tester.pump();
      expect(state.lastIntent, isNull);
    }, variant: macOSOnly);

    testWidgets('line modifier + arrow key movement', (WidgetTester tester) async {
      tester.binding.testTextInput.unregister();
      addTearDown((){
        tester.binding.testTextInput.register();
      });
      final FocusNode editable = FocusNode();
      addTearDown(editable.dispose);
      final FocusNode spy = FocusNode();
      addTearDown(spy.dispose);
      await tester.pumpWidget(
        buildSpyAboveEditableText(
          editableFocusNode: editable,
          spyFocusNode: spy,
        ),
      );
      editable.requestFocus();
      await tester.pump();
      final ActionSpyState state = tester.state<ActionSpyState>(find.byType(ActionSpy));

      await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.arrowLeft, meta: true));
      await tester.pump();
      expect(state.lastIntent, isNull);

      await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.arrowLeft, meta: true));
      await tester.pump();
      expect(state.lastIntent, isNull);

      await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.arrowRight, meta: true));
      await tester.pump();
      expect(state.lastIntent, isNull);

      await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.arrowRight, meta: true));
      await tester.pump();
      expect(state.lastIntent, isNull);
    }, variant: macOSOnly);
  });

  group('macOS does accept shortcuts if focus above EditableText', () {
    final TargetPlatformVariant macOSOnly = TargetPlatformVariant.only(TargetPlatform.macOS);

    testWidgets('word modifier + arrowLeft', (WidgetTester tester) async {
      tester.binding.testTextInput.unregister();
      addTearDown((){
        tester.binding.testTextInput.register();
      });
      final FocusNode editable = FocusNode();
      addTearDown(editable.dispose);
      final FocusNode spy = FocusNode();
      addTearDown(spy.dispose);
      await tester.pumpWidget(
        buildSpyAboveEditableText(
          editableFocusNode: editable,
          spyFocusNode: spy,
        ),
      );
      spy.requestFocus();
      await tester.pump();
      final ActionSpyState state = tester.state<ActionSpyState>(find.byType(ActionSpy));

      await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.arrowLeft, alt: true));
      await tester.pump();

      expect(state.lastIntent, isA<ExtendSelectionToNextWordBoundaryIntent>());
    }, variant: macOSOnly);

    testWidgets('word modifier + arrowRight', (WidgetTester tester) async {
      tester.binding.testTextInput.unregister();
      addTearDown((){
        tester.binding.testTextInput.register();
      });
      final FocusNode editable = FocusNode();
      addTearDown(editable.dispose);
      final FocusNode spy = FocusNode();
      addTearDown(spy.dispose);
      await tester.pumpWidget(
        buildSpyAboveEditableText(
          editableFocusNode: editable,
          spyFocusNode: spy,
        ),
      );
      spy.requestFocus();
      await tester.pump();
      final ActionSpyState state = tester.state<ActionSpyState>(find.byType(ActionSpy));

      await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.arrowRight, alt: true));
      await tester.pump();

      expect(state.lastIntent, isA<ExtendSelectionToNextWordBoundaryIntent>());
    }, variant: macOSOnly);

    testWidgets('line modifier + arrowLeft', (WidgetTester tester) async {
      tester.binding.testTextInput.unregister();
      addTearDown((){
        tester.binding.testTextInput.register();
      });
      final FocusNode editable = FocusNode();
      addTearDown(editable.dispose);
      final FocusNode spy = FocusNode();
      addTearDown(spy.dispose);
      await tester.pumpWidget(
        buildSpyAboveEditableText(
          editableFocusNode: editable,
          spyFocusNode: spy,
        ),
      );
      spy.requestFocus();
      await tester.pump();
      final ActionSpyState state = tester.state<ActionSpyState>(find.byType(ActionSpy));

      await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.arrowLeft, meta: true));
      await tester.pump();

      expect(state.lastIntent, isA<ExtendSelectionToLineBreakIntent>());
    }, variant: macOSOnly);

    testWidgets('line modifier + arrowRight', (WidgetTester tester) async {
      tester.binding.testTextInput.unregister();
      addTearDown((){
        tester.binding.testTextInput.register();
      });
      final FocusNode editable = FocusNode();
      addTearDown(editable.dispose);
      final FocusNode spy = FocusNode();
      addTearDown(spy.dispose);
      await tester.pumpWidget(
        buildSpyAboveEditableText(
          editableFocusNode: editable,
          spyFocusNode: spy,
        ),
      );
      spy.requestFocus();
      await tester.pump();
      final ActionSpyState state = tester.state<ActionSpyState>(find.byType(ActionSpy));

      await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.arrowRight, meta: true));
      await tester.pump();

      expect(state.lastIntent, isA<ExtendSelectionToLineBreakIntent>());
    }, variant: macOSOnly);

    testWidgets('word modifier + arrow key movement', (WidgetTester tester) async {
      tester.binding.testTextInput.unregister();
      addTearDown((){
        tester.binding.testTextInput.register();
      });
      final FocusNode editable = FocusNode();
      addTearDown(editable.dispose);
      final FocusNode spy = FocusNode();
      addTearDown(spy.dispose);
      await tester.pumpWidget(
        buildSpyAboveEditableText(
          editableFocusNode: editable,
          spyFocusNode: spy,
        ),
      );
      spy.requestFocus();
      await tester.pump();
      final ActionSpyState state = tester.state<ActionSpyState>(find.byType(ActionSpy));

      await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.arrowLeft, alt: true));
      await tester.pump();
      expect(state.lastIntent, isA<ExtendSelectionToNextWordBoundaryIntent>());

      state.lastIntent = null;
      await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.arrowLeft, alt: true));
      await tester.pump();
      expect(state.lastIntent, isA<ExtendSelectionToNextWordBoundaryIntent>());

      state.lastIntent = null;
      await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.arrowRight, alt: true));
      await tester.pump();
      expect(state.lastIntent, isA<ExtendSelectionToNextWordBoundaryIntent>());

      state.lastIntent = null;
      await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.arrowRight, alt: true));
      await tester.pump();
      expect(state.lastIntent, isA<ExtendSelectionToNextWordBoundaryIntent>());
    }, variant: macOSOnly);

    testWidgets('line modifier + arrow key movement', (WidgetTester tester) async {
      tester.binding.testTextInput.unregister();
      addTearDown((){
        tester.binding.testTextInput.register();
      });
      final FocusNode editable = FocusNode();
      addTearDown(editable.dispose);
      final FocusNode spy = FocusNode();
      addTearDown(spy.dispose);
      await tester.pumpWidget(
        buildSpyAboveEditableText(
          editableFocusNode: editable,
          spyFocusNode: spy,
        ),
      );
      spy.requestFocus();
      await tester.pump();
      final ActionSpyState state = tester.state<ActionSpyState>(find.byType(ActionSpy));

      await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.arrowLeft, meta: true));
      await tester.pump();
      expect(state.lastIntent, isA<ExtendSelectionToLineBreakIntent>());

      state.lastIntent = null;
      await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.arrowLeft, meta: true));
      await tester.pump();
      expect(state.lastIntent, isA<ExtendSelectionToLineBreakIntent>());

      state.lastIntent = null;
      await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.arrowRight, meta: true));
      await tester.pump();
      expect(state.lastIntent, isA<ExtendSelectionToLineBreakIntent>());

      state.lastIntent = null;
      await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.arrowRight, meta: true));
      await tester.pump();
      expect(state.lastIntent, isA<ExtendSelectionToLineBreakIntent>());
    }, variant: macOSOnly);
  }, skip: kIsWeb); // [intended] specific tests target non-web.
}

class ActionSpy extends StatefulWidget {
  const ActionSpy({super.key, required this.focusNode, required this.child});
  final FocusNode focusNode;
  final Widget child;

  @override
  State<ActionSpy> createState() => ActionSpyState();
}

class ActionSpyState extends State<ActionSpy> {
  Intent? lastIntent;
  late final Map<Type, Action<Intent>> _actions = <Type, Action<Intent>>{
    ExtendSelectionByCharacterIntent: CallbackAction<ExtendSelectionByCharacterIntent>(onInvoke: _captureIntent),
    ExtendSelectionToNextWordBoundaryIntent: CallbackAction<ExtendSelectionToNextWordBoundaryIntent>(onInvoke: _captureIntent),
    ExtendSelectionToLineBreakIntent: CallbackAction<ExtendSelectionToLineBreakIntent>(onInvoke: _captureIntent),
    ExpandSelectionToLineBreakIntent: CallbackAction<ExpandSelectionToLineBreakIntent>(onInvoke: _captureIntent),
    ExpandSelectionToDocumentBoundaryIntent: CallbackAction<ExpandSelectionToDocumentBoundaryIntent>(onInvoke: _captureIntent),
    ExtendSelectionVerticallyToAdjacentLineIntent: CallbackAction<ExtendSelectionVerticallyToAdjacentLineIntent>(onInvoke: _captureIntent),
    ExtendSelectionToDocumentBoundaryIntent: CallbackAction<ExtendSelectionToDocumentBoundaryIntent>(onInvoke: _captureIntent),
    ExtendSelectionToNextWordBoundaryOrCaretLocationIntent: CallbackAction<ExtendSelectionToNextWordBoundaryOrCaretLocationIntent>(onInvoke: _captureIntent),

    DeleteToLineBreakIntent: CallbackAction<DeleteToLineBreakIntent>(onInvoke: _captureIntent),
    DeleteToNextWordBoundaryIntent: CallbackAction<DeleteToNextWordBoundaryIntent>(onInvoke: _captureIntent),
    DeleteCharacterIntent: CallbackAction<DeleteCharacterIntent>(onInvoke: _captureIntent),
  };

  // ignore: use_setters_to_change_properties
  void _captureIntent(Intent intent) {
    lastIntent = intent;
  }

  @override
  Widget build(BuildContext context) {
    return Actions(
      actions: _actions,
      child: Focus(
        focusNode: widget.focusNode,
        child: widget.child,
      ),
    );
  }
}