// 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 'dart:ui' as ui;

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

void main() {
  testWidgetsWithLeakTracking('HardwareKeyboard records pressed keys and enabled locks', (WidgetTester tester) async {
    await simulateKeyDownEvent(LogicalKeyboardKey.numLock, platform: 'windows');
    expect(HardwareKeyboard.instance.physicalKeysPressed,
      equals(<PhysicalKeyboardKey>{PhysicalKeyboardKey.numLock}));
    expect(HardwareKeyboard.instance.logicalKeysPressed,
      equals(<LogicalKeyboardKey>{LogicalKeyboardKey.numLock}));
    expect(HardwareKeyboard.instance.lockModesEnabled,
      equals(<KeyboardLockMode>{KeyboardLockMode.numLock}));

    await simulateKeyDownEvent(LogicalKeyboardKey.numpad1, platform: 'windows');
    expect(HardwareKeyboard.instance.physicalKeysPressed,
      equals(<PhysicalKeyboardKey>{PhysicalKeyboardKey.numLock, PhysicalKeyboardKey.numpad1}));
    expect(HardwareKeyboard.instance.logicalKeysPressed,
      equals(<LogicalKeyboardKey>{LogicalKeyboardKey.numLock, LogicalKeyboardKey.numpad1}));
    expect(HardwareKeyboard.instance.lockModesEnabled,
      equals(<KeyboardLockMode>{KeyboardLockMode.numLock}));

    await simulateKeyRepeatEvent(LogicalKeyboardKey.numpad1, platform: 'windows');
    expect(HardwareKeyboard.instance.physicalKeysPressed,
      equals(<PhysicalKeyboardKey>{PhysicalKeyboardKey.numLock, PhysicalKeyboardKey.numpad1}));
    expect(HardwareKeyboard.instance.logicalKeysPressed,
      equals(<LogicalKeyboardKey>{LogicalKeyboardKey.numLock, LogicalKeyboardKey.numpad1}));
    expect(HardwareKeyboard.instance.lockModesEnabled,
      equals(<KeyboardLockMode>{KeyboardLockMode.numLock}));

    await simulateKeyUpEvent(LogicalKeyboardKey.numLock);
    expect(HardwareKeyboard.instance.physicalKeysPressed,
      equals(<PhysicalKeyboardKey>{PhysicalKeyboardKey.numpad1}));
    expect(HardwareKeyboard.instance.logicalKeysPressed,
      equals(<LogicalKeyboardKey>{LogicalKeyboardKey.numpad1}));
    expect(HardwareKeyboard.instance.lockModesEnabled,
      equals(<KeyboardLockMode>{KeyboardLockMode.numLock}));

    await simulateKeyDownEvent(LogicalKeyboardKey.numLock, platform: 'windows');
    expect(HardwareKeyboard.instance.physicalKeysPressed,
      equals(<PhysicalKeyboardKey>{PhysicalKeyboardKey.numLock, PhysicalKeyboardKey.numpad1}));
    expect(HardwareKeyboard.instance.logicalKeysPressed,
      equals(<LogicalKeyboardKey>{LogicalKeyboardKey.numLock, LogicalKeyboardKey.numpad1}));
    expect(HardwareKeyboard.instance.lockModesEnabled,
      equals(<KeyboardLockMode>{}));

    await simulateKeyUpEvent(LogicalKeyboardKey.numpad1, platform: 'windows');
    expect(HardwareKeyboard.instance.physicalKeysPressed,
      equals(<PhysicalKeyboardKey>{PhysicalKeyboardKey.numLock}));
    expect(HardwareKeyboard.instance.logicalKeysPressed,
      equals(<LogicalKeyboardKey>{LogicalKeyboardKey.numLock}));
    expect(HardwareKeyboard.instance.lockModesEnabled,
      equals(<KeyboardLockMode>{}));

    await simulateKeyUpEvent(LogicalKeyboardKey.numLock, platform: 'windows');
    expect(HardwareKeyboard.instance.physicalKeysPressed,
      equals(<PhysicalKeyboardKey>{}));
    expect(HardwareKeyboard.instance.logicalKeysPressed,
      equals(<LogicalKeyboardKey>{}));
    expect(HardwareKeyboard.instance.lockModesEnabled,
      equals(<KeyboardLockMode>{}));
  // ignore: deprecated_member_use
  }, variant: KeySimulatorTransitModeVariant.keyDataThenRawKeyData());

  testWidgetsWithLeakTracking('KeyEvent can tell which keys are pressed', (WidgetTester tester) async {
    await tester.pumpWidget(const Focus(autofocus: true, child: SizedBox()));
    await tester.pump();

    await simulateKeyDownEvent(LogicalKeyboardKey.numLock, platform: 'windows');

    expect(HardwareKeyboard.instance.isPhysicalKeyPressed(PhysicalKeyboardKey.numLock), isTrue);
    expect(HardwareKeyboard.instance.isLogicalKeyPressed(LogicalKeyboardKey.numLock), isTrue);

    await simulateKeyDownEvent(LogicalKeyboardKey.numpad1, platform: 'windows');
    expect(HardwareKeyboard.instance.isPhysicalKeyPressed(PhysicalKeyboardKey.numpad1), isTrue);
    expect(HardwareKeyboard.instance.isLogicalKeyPressed(LogicalKeyboardKey.numpad1), isTrue);

    await simulateKeyRepeatEvent(LogicalKeyboardKey.numpad1, platform: 'windows');
    expect(HardwareKeyboard.instance.isPhysicalKeyPressed(PhysicalKeyboardKey.numpad1), isTrue);
    expect(HardwareKeyboard.instance.isLogicalKeyPressed(LogicalKeyboardKey.numpad1), isTrue);

    await simulateKeyUpEvent(LogicalKeyboardKey.numLock);
    expect(HardwareKeyboard.instance.isPhysicalKeyPressed(PhysicalKeyboardKey.numpad1), isTrue);
    expect(HardwareKeyboard.instance.isLogicalKeyPressed(LogicalKeyboardKey.numpad1), isTrue);

    await simulateKeyDownEvent(LogicalKeyboardKey.numLock, platform: 'windows');
    expect(HardwareKeyboard.instance.isPhysicalKeyPressed(PhysicalKeyboardKey.numLock), isTrue);
    expect(HardwareKeyboard.instance.isLogicalKeyPressed(LogicalKeyboardKey.numLock), isTrue);

    await simulateKeyUpEvent(LogicalKeyboardKey.numpad1, platform: 'windows');
    expect(HardwareKeyboard.instance.isPhysicalKeyPressed(PhysicalKeyboardKey.numpad1), isFalse);
    expect(HardwareKeyboard.instance.isLogicalKeyPressed(LogicalKeyboardKey.numpad1), isFalse);

    await simulateKeyUpEvent(LogicalKeyboardKey.numLock, platform: 'windows');
    expect(HardwareKeyboard.instance.isPhysicalKeyPressed(PhysicalKeyboardKey.numLock), isFalse);
    expect(HardwareKeyboard.instance.isLogicalKeyPressed(LogicalKeyboardKey.numLock), isFalse);
  // ignore: deprecated_member_use
  }, variant: KeySimulatorTransitModeVariant.keyDataThenRawKeyData());

  testWidgetsWithLeakTracking('KeyboardManager synthesizes modifier keys in rawKeyData mode', (WidgetTester tester) async {
    final List<KeyEvent> events = <KeyEvent>[];
    HardwareKeyboard.instance.addHandler((KeyEvent event) {
      events.add(event);
      return false;
    });
    // While ShiftLeft is held (the event of which was skipped), press keyA.
    final Map<String, dynamic> rawMessage = kIsWeb ? (
      KeyEventSimulator.getKeyData(
        LogicalKeyboardKey.keyA,
        platform: 'web',
      )..['metaState'] = RawKeyEventDataWeb.modifierShift
    ) : (
      KeyEventSimulator.getKeyData(
        LogicalKeyboardKey.keyA,
        platform: 'android',
      )..['metaState'] = RawKeyEventDataAndroid.modifierLeftShift | RawKeyEventDataAndroid.modifierShift
    );
    tester.binding.keyEventManager.handleRawKeyMessage(rawMessage);
    expect(events, hasLength(2));
    expect(events[0].physicalKey, PhysicalKeyboardKey.shiftLeft);
    expect(events[0].logicalKey, LogicalKeyboardKey.shiftLeft);
    expect(events[0].synthesized, true);
    expect(events[1].physicalKey, PhysicalKeyboardKey.keyA);
    expect(events[1].logicalKey, LogicalKeyboardKey.keyA);
    expect(events[1].synthesized, false);
  });

  testWidgetsWithLeakTracking('Dispatch events to all handlers', (WidgetTester tester) async {
    final FocusNode focusNode = FocusNode();
    addTearDown(focusNode.dispose);
    final List<int> logs = <int>[];

    await tester.pumpWidget(
      KeyboardListener(
        autofocus: true,
        focusNode: focusNode,
        child: Container(),
        onKeyEvent: (KeyEvent event) {
          logs.add(1);
        },
      ),
    );

    // Only the Service binding handler.

    expect(await simulateKeyDownEvent(LogicalKeyboardKey.keyA),
      false);
    expect(logs, <int>[1]);
    logs.clear();

    // Add a handler.

    bool handler2Result = false;
    bool handler2(KeyEvent event) {
      logs.add(2);
      return handler2Result;
    }
    HardwareKeyboard.instance.addHandler(handler2);

    expect(await simulateKeyUpEvent(LogicalKeyboardKey.keyA),
      false);
    expect(logs, <int>[2, 1]);
    logs.clear();

    handler2Result = true;

    expect(await simulateKeyDownEvent(LogicalKeyboardKey.keyA),
      true);
    expect(logs, <int>[2, 1]);
    logs.clear();

    // Add another handler.

    handler2Result = false;
    bool handler3Result = false;
    bool handler3(KeyEvent event) {
      logs.add(3);
      return handler3Result;
    }
    HardwareKeyboard.instance.addHandler(handler3);

    expect(await simulateKeyUpEvent(LogicalKeyboardKey.keyA),
      false);
    expect(logs, <int>[2, 3, 1]);
    logs.clear();

    handler2Result = true;

    expect(await simulateKeyDownEvent(LogicalKeyboardKey.keyA),
      true);
    expect(logs, <int>[2, 3, 1]);
    logs.clear();

    handler3Result = true;

    expect(await simulateKeyUpEvent(LogicalKeyboardKey.keyA),
      true);
    expect(logs, <int>[2, 3, 1]);
    logs.clear();

    // Add handler2 again.

    HardwareKeyboard.instance.addHandler(handler2);

    handler3Result = false;
    handler2Result = false;
    expect(await simulateKeyDownEvent(LogicalKeyboardKey.keyA),
      false);
    expect(logs, <int>[2, 3, 2, 1]);
    logs.clear();

    handler2Result = true;
    expect(await simulateKeyUpEvent(LogicalKeyboardKey.keyA),
      true);
    expect(logs, <int>[2, 3, 2, 1]);
    logs.clear();

    // Remove handler2 once.

    HardwareKeyboard.instance.removeHandler(handler2);
    expect(await simulateKeyDownEvent(LogicalKeyboardKey.keyA),
      true);
    expect(logs, <int>[3, 2, 1]);
    logs.clear();
  // ignore: deprecated_member_use
  }, variant: KeySimulatorTransitModeVariant.all());

  // Regression test for https://github.com/flutter/flutter/issues/99196 .
  //
  // In rawKeyData mode, if a key down event is dispatched but immediately
  // synthesized to be released, the old logic would trigger a Null check
  // _CastError on _hardwareKeyboard.lookUpLayout(key). The original scenario
  // that this is triggered on Android is unknown. Here we make up a scenario
  // where a ShiftLeft key down is dispatched but the modifier bit is not set.
  testWidgetsWithLeakTracking('Correctly convert down events that are synthesized released', (WidgetTester tester) async {
    final FocusNode focusNode = FocusNode();
    addTearDown(focusNode.dispose);
    final List<KeyEvent> events = <KeyEvent>[];

    await tester.pumpWidget(
      KeyboardListener(
        autofocus: true,
        focusNode: focusNode,
        child: Container(),
        onKeyEvent: (KeyEvent event) {
          events.add(event);
        },
      ),
    );

    // Dispatch an arbitrary event to bypass the pressedKeys check.
    await simulateKeyDownEvent(LogicalKeyboardKey.keyA, platform: 'web');

    // Dispatch an
    final Map<String, dynamic> data2 = KeyEventSimulator.getKeyData(
      LogicalKeyboardKey.shiftLeft,
      platform: 'web',
    )..['metaState'] = 0;
    await tester.binding.defaultBinaryMessenger.handlePlatformMessage(
      SystemChannels.keyEvent.name,
      SystemChannels.keyEvent.codec.encodeMessage(data2),
      (ByteData? data) {},
    );

    expect(events, hasLength(3));
    expect(events[1], isA<KeyDownEvent>());
    expect(events[1].logicalKey, LogicalKeyboardKey.shiftLeft);
    expect(events[1].synthesized, false);
    expect(events[2], isA<KeyUpEvent>());
    expect(events[2].logicalKey, LogicalKeyboardKey.shiftLeft);
    expect(events[2].synthesized, true);
    expect(ServicesBinding.instance.keyboard.physicalKeysPressed, equals(<PhysicalKeyboardKey>{
      PhysicalKeyboardKey.keyA,
    }));
  // ignore: deprecated_member_use
  }, variant: const KeySimulatorTransitModeVariant(<KeyDataTransitMode>{
    KeyDataTransitMode.rawKeyData,
  }));

  testWidgetsWithLeakTracking('Instantly dispatch synthesized key events when the queue is empty', (WidgetTester tester) async {
    final FocusNode focusNode = FocusNode();
    addTearDown(focusNode.dispose);
    final List<int> logs = <int>[];

    await tester.pumpWidget(
      KeyboardListener(
        autofocus: true,
        focusNode: focusNode,
        child: Container(),
        onKeyEvent: (KeyEvent event) {
          logs.add(1);
        },
      ),
    );
    ServicesBinding.instance.keyboard.addHandler((KeyEvent event) {
      logs.add(2);
      return false;
    });

    // Dispatch a solitary synthesized event.
    expect(ServicesBinding.instance.keyEventManager.handleKeyData(ui.KeyData(
      timeStamp: Duration.zero,
      type: ui.KeyEventType.down,
      logical: LogicalKeyboardKey.keyA.keyId,
      physical: PhysicalKeyboardKey.keyA.usbHidUsage,
      character: null,
      synthesized: true,
    )), false);
    expect(logs, <int>[2, 1]);
    logs.clear();
  // ignore: deprecated_member_use
  }, variant: KeySimulatorTransitModeVariant.keyDataThenRawKeyData());

  testWidgetsWithLeakTracking('Postpone synthesized key events when the queue is not empty', (WidgetTester tester) async {
    final FocusNode keyboardListenerFocusNode = FocusNode();
    addTearDown(keyboardListenerFocusNode.dispose);
    final FocusNode rawKeyboardListenerFocusNode = FocusNode();
    addTearDown(rawKeyboardListenerFocusNode.dispose);
    final List<String> logs = <String>[];

    await tester.pumpWidget(
      RawKeyboardListener(
        focusNode: rawKeyboardListenerFocusNode,
        onKey: (RawKeyEvent event) {
          logs.add('${event.runtimeType}');
        },
        child: KeyboardListener(
          autofocus: true,
          focusNode: keyboardListenerFocusNode,
          child: Container(),
          onKeyEvent: (KeyEvent event) {
            logs.add('${event.runtimeType}');
          },
        ),
      ),
    );

    // On macOS, a CapsLock tap yields a down event and a synthesized up event.
    expect(ServicesBinding.instance.keyEventManager.handleKeyData(ui.KeyData(
      timeStamp: Duration.zero,
      type: ui.KeyEventType.down,
      logical: LogicalKeyboardKey.capsLock.keyId,
      physical: PhysicalKeyboardKey.capsLock.usbHidUsage,
      character: null,
      synthesized: false,
    )), false);
    expect(ServicesBinding.instance.keyEventManager.handleKeyData(ui.KeyData(
      timeStamp: Duration.zero,
      type: ui.KeyEventType.up,
      logical: LogicalKeyboardKey.capsLock.keyId,
      physical: PhysicalKeyboardKey.capsLock.usbHidUsage,
      character: null,
      synthesized: true,
    )), false);
    expect(await ServicesBinding.instance.keyEventManager.handleRawKeyMessage(<String, dynamic>{
      'type': 'keydown',
      'keymap': 'macos',
      'keyCode': 0x00000039,
      'characters': '',
      'charactersIgnoringModifiers': '',
      'modifiers': 0x10000,
    }), equals(<String, dynamic>{'handled': false}));

    expect(logs, <String>['RawKeyDownEvent', 'KeyDownEvent', 'KeyUpEvent']);
    logs.clear();
  // ignore: deprecated_member_use
  }, variant: KeySimulatorTransitModeVariant.keyDataThenRawKeyData());

  // The first key data received from the engine might be an empty key data.
  // In that case, the key data should not be converted to any [KeyEvent]s,
  // but is only used so that *a* key data comes before the raw key message
  // and makes [KeyEventManager] infer [KeyDataTransitMode.keyDataThenRawKeyData].
  testWidgetsWithLeakTracking('Empty keyData yields no event but triggers inference', (WidgetTester tester) async {
    final List<KeyEvent> events = <KeyEvent>[];
    final List<RawKeyEvent> rawEvents = <RawKeyEvent>[];
    tester.binding.keyboard.addHandler((KeyEvent event) {
      events.add(event);
      return true;
    });
    RawKeyboard.instance.addListener((RawKeyEvent event) {
      rawEvents.add(event);
    });
    tester.binding.keyEventManager.handleKeyData(const ui.KeyData(
      type: ui.KeyEventType.down,
      timeStamp: Duration.zero,
      logical: 0,
      physical: 0,
      character: 'a',
      synthesized: false,
    ));
    tester.binding.keyEventManager.handleRawKeyMessage(<String, dynamic>{
      'type': 'keydown',
      'keymap': 'windows',
      'keyCode': 0x04,
      'scanCode': 0x04,
      'characterCodePoint': 0,
      'modifiers': 0,
    });
    expect(events.length, 0);
    expect(rawEvents.length, 1);

    // Dispatch another key data to ensure it's in
    // [KeyDataTransitMode.keyDataThenRawKeyData] mode (otherwise assertion
    // will be thrown upon a KeyData).
    tester.binding.keyEventManager.handleKeyData(const ui.KeyData(
      type: ui.KeyEventType.down,
      timeStamp: Duration.zero,
      logical: 0x22,
      physical: 0x70034,
      character: '"',
      synthesized: false,
    ));
    tester.binding.keyEventManager.handleRawKeyMessage(<String, dynamic>{
      'type': 'keydown',
      'keymap': 'windows',
      'keyCode': 0x04,
      'scanCode': 0x04,
      'characterCodePoint': 0,
      'modifiers': 0,
    });
    expect(events.length, 1);
    expect(rawEvents.length, 2);
  });

  testWidgetsWithLeakTracking('Exceptions from keyMessageHandler are caught and reported', (WidgetTester tester) async {
    final KeyMessageHandler? oldKeyMessageHandler = tester.binding.keyEventManager.keyMessageHandler;
    addTearDown(() {
      tester.binding.keyEventManager.keyMessageHandler = oldKeyMessageHandler;
    });

    // When keyMessageHandler throws an error...
    tester.binding.keyEventManager.keyMessageHandler = (KeyMessage message) {
      throw 1;
    };

    // Simulate a key down event.
    FlutterErrorDetails? record;
    await _runWhileOverridingOnError(
      () => simulateKeyDownEvent(LogicalKeyboardKey.keyA),
      onError: (FlutterErrorDetails details) {
        record = details;
      }
    );

    // ... the error should be caught.
    expect(record, isNotNull);
    expect(record!.exception, 1);
    final Map<String, DiagnosticsNode> infos = _groupDiagnosticsByName(record!.informationCollector!());
    expect(infos['KeyMessage'], isA<DiagnosticsProperty<KeyMessage>>());

    // But the exception should not interrupt recording the state.
    // Now the keyMessageHandler no longer throws an error.
    tester.binding.keyEventManager.keyMessageHandler = null;
    record = null;

    // Simulate a key up event.
    await _runWhileOverridingOnError(
      () => simulateKeyUpEvent(LogicalKeyboardKey.keyA),
      onError: (FlutterErrorDetails details) {
        record = details;
      }
    );
    // If the previous state (key down) wasn't recorded, this key up event will
    // trigger assertions.
    expect(record, isNull);
  });

  testWidgetsWithLeakTracking('Exceptions from HardwareKeyboard handlers are caught and reported', (WidgetTester tester) async {
    bool throwingCallback(KeyEvent event) {
      throw 1;
    }

    // When the handler throws an error...
    HardwareKeyboard.instance.addHandler(throwingCallback);

    // Simulate a key down event.
    FlutterErrorDetails? record;
    await _runWhileOverridingOnError(
      () => simulateKeyDownEvent(LogicalKeyboardKey.keyA),
      onError: (FlutterErrorDetails details) {
        record = details;
      }
    );

    // ... the error should be caught.
    expect(record, isNotNull);
    expect(record!.exception, 1);
    final Map<String, DiagnosticsNode> infos = _groupDiagnosticsByName(record!.informationCollector!());
    expect(infos['Event'], isA<DiagnosticsProperty<KeyEvent>>());

    // But the exception should not interrupt recording the state.
    // Now the key handler no longer throws an error.
    HardwareKeyboard.instance.removeHandler(throwingCallback);
    record = null;

    // Simulate a key up event.
    await _runWhileOverridingOnError(
      () => simulateKeyUpEvent(LogicalKeyboardKey.keyA),
      onError: (FlutterErrorDetails details) {
        record = details;
      }
    );
    // If the previous state (key down) wasn't recorded, this key up event will
    // trigger assertions.
    expect(record, isNull);
  // ignore: deprecated_member_use
  }, variant: KeySimulatorTransitModeVariant.all());

  testWidgetsWithLeakTracking('debugPrintKeyboardEvents causes logging of key events', (WidgetTester tester) async {
    final bool oldDebugPrintKeyboardEvents = debugPrintKeyboardEvents;
    final DebugPrintCallback oldDebugPrint = debugPrint;
    final StringBuffer messages = StringBuffer();
    debugPrint = (String? message, {int? wrapWidth}) {
      messages.writeln(message ?? '');
    };
    debugPrintKeyboardEvents = true;
    try {
      await simulateKeyDownEvent(LogicalKeyboardKey.keyA);
    } finally {
      debugPrintKeyboardEvents = oldDebugPrintKeyboardEvents;
      debugPrint = oldDebugPrint;
    }
    final String messagesStr = messages.toString();
    expect(messagesStr, contains('KEYBOARD: Key event received: '));
    expect(messagesStr, contains('KEYBOARD: Pressed state before processing the event:'));
    expect(messagesStr, contains('KEYBOARD: Pressed state after processing the event:'));
  });
}

Future<void> _runWhileOverridingOnError(AsyncCallback body, {required FlutterExceptionHandler onError}) async {
  final FlutterExceptionHandler? oldFlutterErrorOnError = FlutterError.onError;
  FlutterError.onError = onError;

  try {
    await body();
  } finally {
    FlutterError.onError = oldFlutterErrorOnError;
  }
}

Map<String, DiagnosticsNode> _groupDiagnosticsByName(Iterable<DiagnosticsNode> infos) {
  return Map<String, DiagnosticsNode>.fromIterable(
    infos,
    key: (dynamic node) => (node as DiagnosticsNode).name ?? '',
  );
}
