// 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/services.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';

const List<String> platforms = <String>['linux', 'macos', 'android', 'fuchsia'];

void _verifyKeyEvent<T extends KeyEvent>(KeyEvent event, PhysicalKeyboardKey physical, LogicalKeyboardKey logical, String? character) {
  expect(event, isA<T>());
  expect(event.physicalKey, physical);
  expect(event.logicalKey, logical);
  expect(event.character, character);
  expect(event.synthesized, false);
}

void _verifyRawKeyEvent<T extends RawKeyEvent>(RawKeyEvent event, PhysicalKeyboardKey physical, LogicalKeyboardKey logical, String? character) {
  expect(event, isA<T>());
  expect(event.physicalKey, physical);
  expect(event.logicalKey, logical);
  expect(event.character, character);
}

Future<void> _shouldThrow<T extends Error>(AsyncValueGetter<void> func) async {
  bool hasError = false;
  try {
    await func();
  } catch (e) {
    expect(e, isA<T>());
    hasError = true;
  } finally {
    expect(hasError, true);
  }
}

void main() {
  testWidgets('simulates keyboard events (RawEvent)', (WidgetTester tester) async {
    debugKeyEventSimulatorTransitModeOverride = KeyDataTransitMode.rawKeyData;

    final List<RawKeyEvent> events = <RawKeyEvent>[];

    final FocusNode focusNode = FocusNode();

    await tester.pumpWidget(
      RawKeyboardListener(
        focusNode: focusNode,
        onKey: events.add,
        child: Container(),
      ),
    );

    focusNode.requestFocus();
    await tester.idle();

    for (final String platform in platforms) {
      await tester.sendKeyEvent(LogicalKeyboardKey.shiftLeft, platform: platform);
      await tester.sendKeyEvent(LogicalKeyboardKey.shift, platform: platform);
      await tester.sendKeyDownEvent(LogicalKeyboardKey.keyA, platform: platform);
      await tester.sendKeyUpEvent(LogicalKeyboardKey.keyA, platform: platform);
      await tester.sendKeyDownEvent(LogicalKeyboardKey.numpad1, platform: platform);
      await tester.sendKeyUpEvent(LogicalKeyboardKey.numpad1, platform: platform);
      await tester.idle();

      expect(events.length, 8);
      for (int i = 0; i < events.length; ++i) {
        final bool isEven = i.isEven;
        if (isEven) {
          expect(events[i].runtimeType, equals(RawKeyDownEvent));
        } else {
          expect(events[i].runtimeType, equals(RawKeyUpEvent));
        }
        if (i < 4) {
          expect(events[i].data.isModifierPressed(ModifierKey.shiftModifier, side: KeyboardSide.left), equals(isEven));
        }
      }
      events.clear();
    }

    await tester.pumpWidget(Container());
    focusNode.dispose();

    debugKeyEventSimulatorTransitModeOverride = null;
  });

  testWidgets('simulates keyboard events (KeyData then RawKeyEvent)', (WidgetTester tester) async {
    debugKeyEventSimulatorTransitModeOverride = KeyDataTransitMode.keyDataThenRawKeyData;

    final List<KeyEvent> events = <KeyEvent>[];

    final FocusNode focusNode = FocusNode();

    await tester.pumpWidget(
      KeyboardListener(
        focusNode: focusNode,
        onKeyEvent: events.add,
        child: Container(),
      ),
    );

    focusNode.requestFocus();
    await tester.idle();

    // Key press shiftLeft
    await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft);
    expect(events.length, 1);
    _verifyKeyEvent<KeyDownEvent>(events[0], PhysicalKeyboardKey.shiftLeft, LogicalKeyboardKey.shiftLeft, null);
    expect(HardwareKeyboard.instance.physicalKeysPressed, equals(<PhysicalKeyboardKey>{PhysicalKeyboardKey.shiftLeft}));
    expect(HardwareKeyboard.instance.logicalKeysPressed, equals(<LogicalKeyboardKey>{LogicalKeyboardKey.shiftLeft}));
    expect(HardwareKeyboard.instance.lockModesEnabled, isEmpty);
    events.clear();

    await tester.sendKeyRepeatEvent(LogicalKeyboardKey.shiftLeft);
    expect(events.length, 1);
    _verifyKeyEvent<KeyRepeatEvent>(events[0], PhysicalKeyboardKey.shiftLeft, LogicalKeyboardKey.shiftLeft, null);
    expect(HardwareKeyboard.instance.physicalKeysPressed, equals(<PhysicalKeyboardKey>{PhysicalKeyboardKey.shiftLeft}));
    expect(HardwareKeyboard.instance.logicalKeysPressed, equals(<LogicalKeyboardKey>{LogicalKeyboardKey.shiftLeft}));
    expect(HardwareKeyboard.instance.lockModesEnabled, isEmpty);
    events.clear();

    await tester.sendKeyUpEvent(LogicalKeyboardKey.shiftLeft);
    expect(events.length, 1);
    _verifyKeyEvent<KeyUpEvent>(events[0], PhysicalKeyboardKey.shiftLeft, LogicalKeyboardKey.shiftLeft, null);
    expect(HardwareKeyboard.instance.physicalKeysPressed, isEmpty);
    expect(HardwareKeyboard.instance.logicalKeysPressed, isEmpty);
    expect(HardwareKeyboard.instance.lockModesEnabled, isEmpty);
    events.clear();

    // Key press keyA
    await tester.sendKeyDownEvent(LogicalKeyboardKey.keyA);
    expect(events.length, 1);
    _verifyKeyEvent<KeyDownEvent>(events[0], PhysicalKeyboardKey.keyA, LogicalKeyboardKey.keyA, 'a');
    expect(HardwareKeyboard.instance.physicalKeysPressed, equals(<PhysicalKeyboardKey>{PhysicalKeyboardKey.keyA}));
    expect(HardwareKeyboard.instance.logicalKeysPressed, equals(<LogicalKeyboardKey>{LogicalKeyboardKey.keyA}));
    expect(HardwareKeyboard.instance.lockModesEnabled, isEmpty);
    events.clear();

    await tester.sendKeyRepeatEvent(LogicalKeyboardKey.keyA);
    _verifyKeyEvent<KeyRepeatEvent>(events[0], PhysicalKeyboardKey.keyA, LogicalKeyboardKey.keyA, 'a');
    expect(HardwareKeyboard.instance.physicalKeysPressed, equals(<PhysicalKeyboardKey>{PhysicalKeyboardKey.keyA}));
    expect(HardwareKeyboard.instance.logicalKeysPressed, equals(<LogicalKeyboardKey>{LogicalKeyboardKey.keyA}));
    expect(HardwareKeyboard.instance.lockModesEnabled, isEmpty);
    events.clear();

    await tester.sendKeyUpEvent(LogicalKeyboardKey.keyA);
    _verifyKeyEvent<KeyUpEvent>(events[0], PhysicalKeyboardKey.keyA, LogicalKeyboardKey.keyA, null);
    expect(HardwareKeyboard.instance.physicalKeysPressed, isEmpty);
    expect(HardwareKeyboard.instance.logicalKeysPressed, isEmpty);
    expect(HardwareKeyboard.instance.lockModesEnabled, isEmpty);
    events.clear();

    // Key press keyA with physical keyQ
    await tester.sendKeyDownEvent(LogicalKeyboardKey.keyA, physicalKey: PhysicalKeyboardKey.keyQ);
    expect(events.length, 1);
    _verifyKeyEvent<KeyDownEvent>(events[0], PhysicalKeyboardKey.keyQ, LogicalKeyboardKey.keyA, 'a');
    expect(HardwareKeyboard.instance.physicalKeysPressed, equals(<PhysicalKeyboardKey>{PhysicalKeyboardKey.keyQ}));
    expect(HardwareKeyboard.instance.logicalKeysPressed, equals(<LogicalKeyboardKey>{LogicalKeyboardKey.keyA}));
    expect(HardwareKeyboard.instance.lockModesEnabled, isEmpty);
    events.clear();

    await tester.sendKeyRepeatEvent(LogicalKeyboardKey.keyA, physicalKey: PhysicalKeyboardKey.keyQ);
    _verifyKeyEvent<KeyRepeatEvent>(events[0], PhysicalKeyboardKey.keyQ, LogicalKeyboardKey.keyA, 'a');
    expect(HardwareKeyboard.instance.physicalKeysPressed, equals(<PhysicalKeyboardKey>{PhysicalKeyboardKey.keyQ}));
    expect(HardwareKeyboard.instance.logicalKeysPressed, equals(<LogicalKeyboardKey>{LogicalKeyboardKey.keyA}));
    expect(HardwareKeyboard.instance.lockModesEnabled, isEmpty);
    events.clear();

    await tester.sendKeyUpEvent(LogicalKeyboardKey.keyA, physicalKey: PhysicalKeyboardKey.keyQ);
    _verifyKeyEvent<KeyUpEvent>(events[0], PhysicalKeyboardKey.keyQ, LogicalKeyboardKey.keyA, null);
    expect(HardwareKeyboard.instance.physicalKeysPressed, isEmpty);
    expect(HardwareKeyboard.instance.logicalKeysPressed, isEmpty);
    expect(HardwareKeyboard.instance.lockModesEnabled, isEmpty);
    events.clear();

    // Key press numpad1
    await tester.sendKeyDownEvent(LogicalKeyboardKey.numpad1);
    _verifyKeyEvent<KeyDownEvent>(events[0], PhysicalKeyboardKey.numpad1, LogicalKeyboardKey.numpad1, null);
    expect(HardwareKeyboard.instance.physicalKeysPressed, equals(<PhysicalKeyboardKey>{PhysicalKeyboardKey.numpad1}));
    expect(HardwareKeyboard.instance.logicalKeysPressed, equals(<LogicalKeyboardKey>{LogicalKeyboardKey.numpad1}));
    expect(HardwareKeyboard.instance.lockModesEnabled, isEmpty);
    events.clear();

    await tester.sendKeyRepeatEvent(LogicalKeyboardKey.numpad1);
    _verifyKeyEvent<KeyRepeatEvent>(events[0], PhysicalKeyboardKey.numpad1, LogicalKeyboardKey.numpad1, null);
    expect(HardwareKeyboard.instance.physicalKeysPressed, equals(<PhysicalKeyboardKey>{PhysicalKeyboardKey.numpad1}));
    expect(HardwareKeyboard.instance.logicalKeysPressed, equals(<LogicalKeyboardKey>{LogicalKeyboardKey.numpad1}));
    expect(HardwareKeyboard.instance.lockModesEnabled, isEmpty);
    events.clear();

    await tester.sendKeyUpEvent(LogicalKeyboardKey.numpad1);
    _verifyKeyEvent<KeyUpEvent>(events[0], PhysicalKeyboardKey.numpad1, LogicalKeyboardKey.numpad1, null);
    expect(HardwareKeyboard.instance.physicalKeysPressed, isEmpty);
    expect(HardwareKeyboard.instance.logicalKeysPressed, isEmpty);
    expect(HardwareKeyboard.instance.lockModesEnabled, isEmpty);
    events.clear();

    // Key press numLock (1st time)
    await tester.sendKeyDownEvent(LogicalKeyboardKey.numLock);
    _verifyKeyEvent<KeyDownEvent>(events[0], PhysicalKeyboardKey.numLock, LogicalKeyboardKey.numLock, null);
    expect(HardwareKeyboard.instance.physicalKeysPressed, equals(<PhysicalKeyboardKey>{PhysicalKeyboardKey.numLock}));
    expect(HardwareKeyboard.instance.logicalKeysPressed, equals(<LogicalKeyboardKey>{LogicalKeyboardKey.numLock}));
    expect(HardwareKeyboard.instance.lockModesEnabled, equals(<KeyboardLockMode>{KeyboardLockMode.numLock}));
    events.clear();

    await tester.sendKeyRepeatEvent(LogicalKeyboardKey.numLock);
    _verifyKeyEvent<KeyRepeatEvent>(events[0], PhysicalKeyboardKey.numLock, LogicalKeyboardKey.numLock, null);
    expect(HardwareKeyboard.instance.physicalKeysPressed, equals(<PhysicalKeyboardKey>{PhysicalKeyboardKey.numLock}));
    expect(HardwareKeyboard.instance.logicalKeysPressed, equals(<LogicalKeyboardKey>{LogicalKeyboardKey.numLock}));
    expect(HardwareKeyboard.instance.lockModesEnabled, equals(<KeyboardLockMode>{KeyboardLockMode.numLock}));
    events.clear();

    await tester.sendKeyUpEvent(LogicalKeyboardKey.numLock);
    _verifyKeyEvent<KeyUpEvent>(events[0], PhysicalKeyboardKey.numLock, LogicalKeyboardKey.numLock, null);
    expect(HardwareKeyboard.instance.physicalKeysPressed, isEmpty);
    expect(HardwareKeyboard.instance.logicalKeysPressed, isEmpty);
    expect(HardwareKeyboard.instance.lockModesEnabled, equals(<KeyboardLockMode>{KeyboardLockMode.numLock}));
    events.clear();

    // Key press numLock (2nd time)
    await tester.sendKeyDownEvent(LogicalKeyboardKey.numLock);
    _verifyKeyEvent<KeyDownEvent>(events[0], PhysicalKeyboardKey.numLock, LogicalKeyboardKey.numLock, null);
    expect(HardwareKeyboard.instance.physicalKeysPressed, equals(<PhysicalKeyboardKey>{PhysicalKeyboardKey.numLock}));
    expect(HardwareKeyboard.instance.logicalKeysPressed, equals(<LogicalKeyboardKey>{LogicalKeyboardKey.numLock}));
    expect(HardwareKeyboard.instance.lockModesEnabled, isEmpty);
    events.clear();

    await tester.sendKeyRepeatEvent(LogicalKeyboardKey.numLock);
    _verifyKeyEvent<KeyRepeatEvent>(events[0], PhysicalKeyboardKey.numLock, LogicalKeyboardKey.numLock, null);
    expect(HardwareKeyboard.instance.physicalKeysPressed, equals(<PhysicalKeyboardKey>{PhysicalKeyboardKey.numLock}));
    expect(HardwareKeyboard.instance.logicalKeysPressed, equals(<LogicalKeyboardKey>{LogicalKeyboardKey.numLock}));
    expect(HardwareKeyboard.instance.lockModesEnabled, isEmpty);
    events.clear();

    await tester.sendKeyUpEvent(LogicalKeyboardKey.numLock);
    _verifyKeyEvent<KeyUpEvent>(events[0], PhysicalKeyboardKey.numLock, LogicalKeyboardKey.numLock, null);
    expect(HardwareKeyboard.instance.physicalKeysPressed, isEmpty);
    expect(HardwareKeyboard.instance.logicalKeysPressed, isEmpty);
    expect(HardwareKeyboard.instance.lockModesEnabled, isEmpty);
    events.clear();

    await tester.idle();

    await tester.pumpWidget(Container());
    focusNode.dispose();

    debugKeyEventSimulatorTransitModeOverride = null;
  });

  testWidgets('simulates using the correct transit mode: rawKeyData', (WidgetTester tester) async {
    debugKeyEventSimulatorTransitModeOverride = KeyDataTransitMode.rawKeyData;

    final List<Object> events = <Object>[];

    final FocusNode focusNode = FocusNode();
    await tester.pumpWidget(
      Focus(
        focusNode: focusNode,
        onKey: (FocusNode node, RawKeyEvent event) {
          events.add(event);
          return KeyEventResult.ignored;
        },
        onKeyEvent: (FocusNode node, KeyEvent event) {
          events.add(event);
          return KeyEventResult.ignored;
        },
        child: Container(),
      ),
    );

    focusNode.requestFocus();
    await tester.idle();

    // A (physical keyA, logical keyA) is pressed.
    await simulateKeyDownEvent(LogicalKeyboardKey.keyA);
    expect(events.length, 2);
    expect(events[0], isA<KeyEvent>());
    _verifyKeyEvent<KeyDownEvent>(events[0] as KeyEvent, PhysicalKeyboardKey.keyA, LogicalKeyboardKey.keyA, 'a');
    expect(events[1], isA<RawKeyEvent>());
    _verifyRawKeyEvent<RawKeyDownEvent>(events[1] as RawKeyEvent, PhysicalKeyboardKey.keyA, LogicalKeyboardKey.keyA, 'a');
    events.clear();

    // A (physical keyA, logical keyB) is released.
    //
    // Since this event was synthesized and regularized before being sent to
    // HardwareKeyboard, this event will be accepted.
    await simulateKeyUpEvent(LogicalKeyboardKey.keyB, physicalKey: PhysicalKeyboardKey.keyA);
    expect(events.length, 2);
    expect(events[0], isA<KeyEvent>());
    _verifyKeyEvent<KeyUpEvent>(events[0] as KeyEvent, PhysicalKeyboardKey.keyA, LogicalKeyboardKey.keyA, null);
    expect(events[1], isA<RawKeyEvent>());
    _verifyRawKeyEvent<RawKeyUpEvent>(events[1] as RawKeyEvent, PhysicalKeyboardKey.keyA, LogicalKeyboardKey.keyB, null);
    events.clear();

    // Manually switch the transit mode to `keyDataThenRawKeyData`. This will
    // never happen in real applications so the assertion error can verify that
    // the transit mode is correctly applied.
    debugKeyEventSimulatorTransitModeOverride = KeyDataTransitMode.keyDataThenRawKeyData;

    await _shouldThrow<AssertionError>(() =>
      simulateKeyUpEvent(LogicalKeyboardKey.keyB, physicalKey: PhysicalKeyboardKey.keyA));

    debugKeyEventSimulatorTransitModeOverride = null;
  });

  testWidgets('simulates using the correct transit mode: keyDataThenRawKeyData', (WidgetTester tester) async {
    debugKeyEventSimulatorTransitModeOverride = KeyDataTransitMode.keyDataThenRawKeyData;

    final List<Object> events = <Object>[];

    final FocusNode focusNode = FocusNode();
    await tester.pumpWidget(
      Focus(
        focusNode: focusNode,
        onKey: (FocusNode node, RawKeyEvent event) {
          events.add(event);
          return KeyEventResult.ignored;
        },
        onKeyEvent: (FocusNode node, KeyEvent event) {
          events.add(event);
          return KeyEventResult.ignored;
        },
        child: Container(),
      ),
    );

    focusNode.requestFocus();
    await tester.idle();

    // A (physical keyA, logical keyA) is pressed.
    await simulateKeyDownEvent(LogicalKeyboardKey.keyA);
    expect(events.length, 2);
    expect(events[0], isA<KeyEvent>());
    _verifyKeyEvent<KeyDownEvent>(events[0] as KeyEvent, PhysicalKeyboardKey.keyA, LogicalKeyboardKey.keyA, 'a');
    expect(events[1], isA<RawKeyEvent>());
    _verifyRawKeyEvent<RawKeyDownEvent>(events[1] as RawKeyEvent, PhysicalKeyboardKey.keyA, LogicalKeyboardKey.keyA, 'a');
    events.clear();

    // A (physical keyA, logical keyB) is released.
    //
    // Since this event is transmitted to HardwareKeyboard as-is, it will be rejected due to
    // inconsistent logical key. This does not indicate behaviral difference,
    // since KeyData is will never send malformed data sequence in real applications.
    await _shouldThrow<AssertionError>(() =>
      simulateKeyUpEvent(LogicalKeyboardKey.keyB, physicalKey: PhysicalKeyboardKey.keyA));

    debugKeyEventSimulatorTransitModeOverride = null;
  });
}