// 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 'editable_text_utils.dart';

final FocusNode focusNode = FocusNode(debugLabel: 'UndoHistory Node');

void main() {
  TestWidgetsFlutterBinding.ensureInitialized();

  group('UndoHistory', () {
    Future<void> sendUndoRedo(WidgetTester tester, [bool redo = false]) {
      return sendKeys(
        tester,
        <LogicalKeyboardKey>[
          LogicalKeyboardKey.keyZ,
        ],
        shortcutModifier: true,
        shift: redo,
        targetPlatform: defaultTargetPlatform,
      );
    }

    Future<void> sendUndo(WidgetTester tester) => sendUndoRedo(tester);
    Future<void> sendRedo(WidgetTester tester) => sendUndoRedo(tester, true);

    testWidgets('allows undo and redo to be called programmatically from the UndoHistoryController', (WidgetTester tester) async {
      final ValueNotifier<int> value = ValueNotifier<int>(0);
      final UndoHistoryController controller = UndoHistoryController();
      await tester.pumpWidget(
        MaterialApp(
          home: UndoHistory<int>(
            value: value,
            controller: controller,
            onTriggered: (int newValue) {
              value.value = newValue;
            },
            focusNode: focusNode,
            child: Container(),
          ),
        ),
      );

      await tester.pump(const Duration(milliseconds: 500));

      // Undo/redo have no effect if the value has never changed.
      expect(controller.value.canUndo, false);
      expect(controller.value.canRedo, false);
      controller.undo();
      expect(value.value, 0);
      controller.redo();
      expect(value.value, 0);

      focusNode.requestFocus();
      await tester.pump();
      expect(controller.value.canUndo, false);
      expect(controller.value.canRedo, false);
      controller.undo();
      expect(value.value, 0);
      controller.redo();
      expect(value.value, 0);

      value.value = 1;

      // Wait for the throttling.
      await tester.pump(const Duration(milliseconds: 500));

      // Can undo/redo a single change.
      expect(controller.value.canUndo, true);
      expect(controller.value.canRedo, false);
      controller.undo();
      expect(value.value, 0);
      expect(controller.value.canUndo, false);
      expect(controller.value.canRedo, true);
      controller.redo();
      expect(value.value, 1);
      expect(controller.value.canUndo, true);
      expect(controller.value.canRedo, false);

      value.value = 2;
      await tester.pump(const Duration(milliseconds: 500));

      // And can undo/redo multiple changes.
      expect(controller.value.canUndo, true);
      expect(controller.value.canRedo, false);
      controller.undo();
      expect(value.value, 1);
      expect(controller.value.canUndo, true);
      expect(controller.value.canRedo, true);
      controller.undo();
      expect(value.value, 0);
      expect(controller.value.canUndo, false);
      expect(controller.value.canRedo, true);
      controller.redo();
      expect(value.value, 1);
      expect(controller.value.canUndo, true);
      expect(controller.value.canRedo, true);
      controller.redo();
      expect(value.value, 2);
      expect(controller.value.canUndo, true);
      expect(controller.value.canRedo, false);

      // Changing the value again clears the redo stack.
      expect(controller.value.canUndo, true);
      expect(controller.value.canRedo, false);
      controller.undo();
      expect(value.value, 1);
      expect(controller.value.canUndo, true);
      expect(controller.value.canRedo, true);
      value.value = 3;
      await tester.pump(const Duration(milliseconds: 500));
      expect(controller.value.canUndo, true);
      expect(controller.value.canRedo, false);
    }, variant: TargetPlatformVariant.all());

    testWidgets('allows undo and redo to be called using the keyboard', (WidgetTester tester) async {
      final ValueNotifier<int> value = ValueNotifier<int>(0);
      final UndoHistoryController controller = UndoHistoryController();
      await tester.pumpWidget(
        MaterialApp(
          home: UndoHistory<int>(
            controller: controller,
            value: value,
            onTriggered: (int newValue) {
              value.value = newValue;
            },
            focusNode: focusNode,
            child: Focus(
              focusNode: focusNode,
              child: Container(),
            ),
          ),
        ),
      );

      await tester.pump(const Duration(milliseconds: 500));

      // Undo/redo have no effect if the value has never changed.
      expect(controller.value.canUndo, false);
      expect(controller.value.canRedo, false);
      await sendUndo(tester);
      expect(value.value, 0);
      await sendRedo(tester);
      expect(value.value, 0);

      focusNode.requestFocus();
      await tester.pump();
      expect(controller.value.canUndo, false);
      expect(controller.value.canRedo, false);
      await sendUndo(tester);
      expect(value.value, 0);
      await sendRedo(tester);
      expect(value.value, 0);

      value.value = 1;

      // Wait for the throttling.
      await tester.pump(const Duration(milliseconds: 500));

      // Can undo/redo a single change.
      expect(controller.value.canUndo, true);
      expect(controller.value.canRedo, false);
      await sendUndo(tester);
      expect(value.value, 0);
      expect(controller.value.canUndo, false);
      expect(controller.value.canRedo, true);
      await sendRedo(tester);
      expect(value.value, 1);
      expect(controller.value.canUndo, true);
      expect(controller.value.canRedo, false);

      value.value = 2;
      await tester.pump(const Duration(milliseconds: 500));

      // And can undo/redo multiple changes.
      expect(controller.value.canUndo, true);
      expect(controller.value.canRedo, false);
      await sendUndo(tester);
      expect(value.value, 1);
      expect(controller.value.canUndo, true);
      expect(controller.value.canRedo, true);
      await sendUndo(tester);
      expect(value.value, 0);
      expect(controller.value.canUndo, false);
      expect(controller.value.canRedo, true);
      await sendRedo(tester);
      expect(value.value, 1);
      expect(controller.value.canUndo, true);
      expect(controller.value.canRedo, true);
      await sendRedo(tester);
      expect(value.value, 2);
      expect(controller.value.canUndo, true);
      expect(controller.value.canRedo, false);

      // Changing the value again clears the redo stack.
      expect(controller.value.canUndo, true);
      expect(controller.value.canRedo, false);
      await sendUndo(tester);
      expect(value.value, 1);
      expect(controller.value.canUndo, true);
      expect(controller.value.canRedo, true);
      value.value = 3;
      await tester.pump(const Duration(milliseconds: 500));
      expect(controller.value.canUndo, true);
      expect(controller.value.canRedo, false);
    }, variant: TargetPlatformVariant.all(), skip: kIsWeb); // [intended]

    testWidgets('duplicate changes do not affect the undo history', (WidgetTester tester) async {
      final ValueNotifier<int> value = ValueNotifier<int>(0);
      final UndoHistoryController controller = UndoHistoryController();
      await tester.pumpWidget(
        MaterialApp(
          home: UndoHistory<int>(
            controller: controller,
            value: value,
            onTriggered: (int newValue) {
              value.value = newValue;
            },
            focusNode: focusNode,
            child: Container(),
          ),
        ),
      );

      focusNode.requestFocus();

      // Wait for the throttling.
      await tester.pump(const Duration(milliseconds: 500));

      value.value = 1;

      // Wait for the throttling.
      await tester.pump(const Duration(milliseconds: 500));

      // Can undo/redo a single change.
      expect(controller.value.canUndo, true);
      expect(controller.value.canRedo, false);
      controller.undo();
      expect(value.value, 0);
      expect(controller.value.canUndo, false);
      expect(controller.value.canRedo, true);
      controller.redo();
      expect(value.value, 1);
      expect(controller.value.canUndo, true);
      expect(controller.value.canRedo, false);

      // Changes that result in the same state won't be saved on the undo stack.
      value.value = 1;
      await tester.pump(const Duration(milliseconds: 500));
      expect(controller.value.canUndo, true);
      expect(controller.value.canRedo, false);
      controller.undo();
      expect(value.value, 0);
      expect(controller.value.canUndo, false);
      expect(controller.value.canRedo, true);
    }, variant: TargetPlatformVariant.all());

    testWidgets('ignores value changes pushed during onTriggered', (WidgetTester tester) async {
      final ValueNotifier<int> value = ValueNotifier<int>(0);
      final UndoHistoryController controller = UndoHistoryController();
      int Function(int newValue) valueToUse = (int value) => value;
      final GlobalKey<UndoHistoryState<int>> key = GlobalKey<UndoHistoryState<int>>();
      await tester.pumpWidget(
        MaterialApp(
          home: UndoHistory<int>(
            key: key,
            value: value,
            controller: controller,
            onTriggered: (int newValue) {
              value.value = valueToUse(newValue);
            },
            focusNode: focusNode,
            child: Container(),
          ),
        ),
      );

      await tester.pump(const Duration(milliseconds: 500));

      // Undo/redo have no effect if the value has never changed.
      expect(controller.value.canUndo, false);
      expect(controller.value.canRedo, false);
      controller.undo();
      expect(value.value, 0);
      controller.redo();
      expect(value.value, 0);

      focusNode.requestFocus();
      await tester.pump();
      expect(controller.value.canUndo, false);
      expect(controller.value.canRedo, false);
      controller.undo();
      expect(value.value, 0);
      controller.redo();
      expect(value.value, 0);

      value.value = 1;

      // Wait for the throttling.
      await tester.pump(const Duration(milliseconds: 500));

      valueToUse = (int value) => 3;
      expect(() => key.currentState!.undo(), throwsAssertionError);
    }, variant: TargetPlatformVariant.all());

    testWidgets('changes should send setUndoState to the UndoManagerConnection on iOS', (WidgetTester tester) async {
      final List<MethodCall> log = <MethodCall>[];
      SystemChannels.undoManager.setMockMethodCallHandler((MethodCall methodCall) async {
        log.add(methodCall);
      });
      final FocusNode focusNode = FocusNode();

      final ValueNotifier<int> value = ValueNotifier<int>(0);
      final UndoHistoryController controller = UndoHistoryController();
      await tester.pumpWidget(
        MaterialApp(
          home: UndoHistory<int>(
            controller: controller,
            value: value,
            onTriggered: (int newValue) {
              value.value = newValue;
            },
            focusNode: focusNode,
            child: Focus(
              focusNode: focusNode,
              child: Container(),
            ),
          ),
        ),
      );

      await tester.pump();

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

      // Wait for the throttling.
      await tester.pump(const Duration(milliseconds: 500));

      // Undo and redo should both be disabled.
      MethodCall methodCall = log.lastWhere((MethodCall m) => m.method == 'UndoManager.setUndoState');
      expect(methodCall.method, 'UndoManager.setUndoState');
      expect(methodCall.arguments as Map<String, dynamic>, <String, bool>{'canUndo': false, 'canRedo': false});

      // Making a change should enable undo.
      value.value = 1;
      await tester.pump(const Duration(milliseconds: 500));

      methodCall = log.lastWhere((MethodCall m) => m.method == 'UndoManager.setUndoState');
      expect(methodCall.method, 'UndoManager.setUndoState');
      expect(methodCall.arguments as Map<String, dynamic>, <String, bool>{'canUndo': true, 'canRedo': false});

      // Undo should remain enabled after another change.
      value.value = 2;
      await tester.pump(const Duration(milliseconds: 500));

      methodCall = log.lastWhere((MethodCall m) => m.method == 'UndoManager.setUndoState');
      expect(methodCall.method, 'UndoManager.setUndoState');
      expect(methodCall.arguments as Map<String, dynamic>, <String, bool>{'canUndo': true, 'canRedo': false});

      // Undo and redo should be enabled after one undo.
      controller.undo();
      methodCall = log.lastWhere((MethodCall m) => m.method == 'UndoManager.setUndoState');
      expect(methodCall.method, 'UndoManager.setUndoState');
      expect(methodCall.arguments as Map<String, dynamic>, <String, bool>{'canUndo': true, 'canRedo': true});

      // Only redo should be enabled after a second undo.
      controller.undo();
      methodCall = log.lastWhere((MethodCall m) => m.method == 'UndoManager.setUndoState');
      expect(methodCall.method, 'UndoManager.setUndoState');
      expect(methodCall.arguments as Map<String, dynamic>, <String, bool>{'canUndo': false, 'canRedo': true});
    }, variant: const TargetPlatformVariant(<TargetPlatform>{TargetPlatform.iOS}), skip: kIsWeb); // [intended]

    testWidgets('handlePlatformUndo should undo or redo appropriately on iOS', (WidgetTester tester) async {
      final ValueNotifier<int> value = ValueNotifier<int>(0);
      final UndoHistoryController controller = UndoHistoryController();
      await tester.pumpWidget(
        MaterialApp(
          home: UndoHistory<int>(
            controller: controller,
            value: value,
            onTriggered: (int newValue) {
              value.value = newValue;
            },
            focusNode: focusNode,
            child: Focus(
              focusNode: focusNode,
              child: Container(),
            ),
          ),
        ),
      );

      await tester.pump(const Duration(milliseconds: 500));
      focusNode.requestFocus();
      await tester.pump();

      // Undo/redo have no effect if the value has never changed.
      expect(controller.value.canUndo, false);
      expect(controller.value.canRedo, false);
      UndoManager.client!.handlePlatformUndo(UndoDirection.undo);
      expect(value.value, 0);
      UndoManager.client!.handlePlatformUndo(UndoDirection.redo);
      expect(value.value, 0);

      value.value = 1;

      // Wait for the throttling.
      await tester.pump(const Duration(milliseconds: 500));

      // Can undo/redo a single change.
      expect(controller.value.canUndo, true);
      expect(controller.value.canRedo, false);
      UndoManager.client!.handlePlatformUndo(UndoDirection.undo);
      expect(value.value, 0);
      expect(controller.value.canUndo, false);
      expect(controller.value.canRedo, true);
      UndoManager.client!.handlePlatformUndo(UndoDirection.redo);
      expect(value.value, 1);
      expect(controller.value.canUndo, true);
      expect(controller.value.canRedo, false);

      value.value = 2;
      await tester.pump(const Duration(milliseconds: 500));

      // And can undo/redo multiple changes.
      expect(controller.value.canUndo, true);
      expect(controller.value.canRedo, false);
      UndoManager.client!.handlePlatformUndo(UndoDirection.undo);
      expect(value.value, 1);
      expect(controller.value.canUndo, true);
      expect(controller.value.canRedo, true);
      UndoManager.client!.handlePlatformUndo(UndoDirection.undo);
      expect(value.value, 0);
      expect(controller.value.canUndo, false);
      expect(controller.value.canRedo, true);
      UndoManager.client!.handlePlatformUndo(UndoDirection.redo);
      expect(value.value, 1);
      expect(controller.value.canUndo, true);
      expect(controller.value.canRedo, true);
      UndoManager.client!.handlePlatformUndo(UndoDirection.redo);
      expect(value.value, 2);
      expect(controller.value.canUndo, true);
      expect(controller.value.canRedo, false);

      // Changing the value again clears the redo stack.
      expect(controller.value.canUndo, true);
      expect(controller.value.canRedo, false);
      UndoManager.client!.handlePlatformUndo(UndoDirection.undo);
      expect(value.value, 1);
      expect(controller.value.canUndo, true);
      expect(controller.value.canRedo, true);
      value.value = 3;
      await tester.pump(const Duration(milliseconds: 500));
      expect(controller.value.canUndo, true);
      expect(controller.value.canRedo, false);
    }, variant: const TargetPlatformVariant(<TargetPlatform>{TargetPlatform.iOS}), skip: kIsWeb); // [intended]
  });

  group('UndoHistoryController', () {
    testWidgets('UndoHistoryController notifies onUndo listeners onUndo', (WidgetTester tester) async {
      int calls = 0;
      final UndoHistoryController controller = UndoHistoryController();
      controller.onUndo.addListener(() {
        calls++;
      });

      // Does not notify the listener if canUndo is false.
      controller.undo();
      expect(calls, 0);

      // Does notify the listener if canUndo is true.
      controller.value = const UndoHistoryValue(canUndo: true);
      controller.undo();
      expect(calls, 1);
    });

    testWidgets('UndoHistoryController notifies onRedo listeners onRedo', (WidgetTester tester) async {
      int calls = 0;
      final UndoHistoryController controller = UndoHistoryController();
      controller.onRedo.addListener(() {
        calls++;
      });

      // Does not notify the listener if canUndo is false.
      controller.redo();
      expect(calls, 0);

      // Does notify the listener if canRedo is true.
      controller.value = const UndoHistoryValue(canRedo: true);
      controller.redo();
      expect(calls, 1);
    });

    testWidgets('UndoHistoryController notifies listeners on value change', (WidgetTester tester) async {
      int calls = 0;
      final UndoHistoryController controller = UndoHistoryController(value: const UndoHistoryValue(canUndo: true));
      controller.addListener(() {
        calls++;
      });

      // Does not notify if the value is the same.
      controller.value = const UndoHistoryValue(canUndo: true);
      expect(calls, 0);

      // Does notify if the value has changed.
      controller.value = const UndoHistoryValue(canRedo: true);
      expect(calls, 1);
    });
  });
}
