// Copyright 2019 The Chromium 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/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';

typedef PostInvokeCallback = void Function({Action action, Intent intent, FocusNode focusNode, ActionDispatcher dispatcher});

class TestAction extends CallbackAction {
  const TestAction({
    @required OnInvokeCallback onInvoke,
  })  : assert(onInvoke != null),
        super(key, onInvoke: onInvoke);

  static const LocalKey key = ValueKey<Type>(TestAction);

  void _testInvoke(FocusNode node, Intent invocation) => invoke(node, invocation);
}

class TestDispatcher extends ActionDispatcher {
  const TestDispatcher({this.postInvoke});

  final PostInvokeCallback postInvoke;

  @override
  bool invokeAction(Action action, Intent intent, {FocusNode focusNode}) {
    final bool result = super.invokeAction(action, intent, focusNode: focusNode);
    postInvoke?.call(action: action, intent: intent, focusNode: focusNode, dispatcher: this);
    return result;
  }
}

class TestDispatcher1 extends TestDispatcher {
  const TestDispatcher1({PostInvokeCallback postInvoke}) : super(postInvoke: postInvoke);
}

void main() {
  test('Action passes parameters on when invoked.', () {
    bool invoked = false;
    FocusNode passedNode;
    final TestAction action = TestAction(onInvoke: (FocusNode node, Intent invocation) {
      invoked = true;
      passedNode = node;
    });
    final FocusNode testNode = FocusNode(debugLabel: 'Test Node');
    action._testInvoke(testNode, null);
    expect(passedNode, equals(testNode));
    expect(action.intentKey, equals(TestAction.key));
    expect(invoked, isTrue);
  });
  group(ActionDispatcher, () {
    test('ActionDispatcher invokes actions when asked.', () {
      bool invoked = false;
      FocusNode passedNode;
      const ActionDispatcher dispatcher = ActionDispatcher();
      final FocusNode testNode = FocusNode(debugLabel: 'Test Node');
      final bool result = dispatcher.invokeAction(
        TestAction(
          onInvoke: (FocusNode node, Intent invocation) {
            invoked = true;
            passedNode = node;
          },
        ),
        const Intent(TestAction.key),
        focusNode: testNode,
      );
      expect(passedNode, equals(testNode));
      expect(result, isTrue);
      expect(invoked, isTrue);
    });
  });
  group(Actions, () {
    Intent invokedIntent;
    Action invokedAction;
    FocusNode invokedNode;
    ActionDispatcher invokedDispatcher;

    void collect({Action action, Intent intent, FocusNode focusNode, ActionDispatcher dispatcher}) {
      invokedIntent = intent;
      invokedAction = action;
      invokedNode = focusNode;
      invokedDispatcher = dispatcher;
    }

    void clear() {
      invokedIntent = null;
      invokedAction = null;
      invokedNode = null;
      invokedDispatcher = null;
    }

    setUp(clear);

    testWidgets('Actions widget can invoke actions with default dispatcher', (WidgetTester tester) async {
      final GlobalKey containerKey = GlobalKey();
      bool invoked = false;
      FocusNode passedNode;
      final FocusNode testNode = FocusNode(debugLabel: 'Test Node');

      await tester.pumpWidget(
        Actions(
          actions: <LocalKey, ActionFactory>{
            TestAction.key: () => TestAction(
                  onInvoke: (FocusNode node, Intent invocation) {
                    invoked = true;
                    passedNode = node;
                  },
                ),
          },
          child: Container(key: containerKey),
        ),
      );

      await tester.pump();
      final bool result = Actions.invoke(
        containerKey.currentContext,
        const Intent(TestAction.key),
        focusNode: testNode,
      );
      expect(passedNode, equals(testNode));
      expect(result, isTrue);
      expect(invoked, isTrue);
    });
    testWidgets('Actions widget can invoke actions with custom dispatcher', (WidgetTester tester) async {
      final GlobalKey containerKey = GlobalKey();
      bool invoked = false;
      const Intent intent = Intent(TestAction.key);
      FocusNode passedNode;
      final FocusNode testNode = FocusNode(debugLabel: 'Test Node');
      final Action testAction = TestAction(
        onInvoke: (FocusNode node, Intent intent) {
          invoked = true;
          passedNode = node;
        },
      );

      await tester.pumpWidget(
        Actions(
          dispatcher: TestDispatcher(postInvoke: collect),
          actions: <LocalKey, ActionFactory>{
            TestAction.key: () => testAction,
          },
          child: Container(key: containerKey),
        ),
      );

      await tester.pump();
      final bool result = Actions.invoke(
        containerKey.currentContext,
        intent,
        focusNode: testNode,
      );
      expect(passedNode, equals(testNode));
      expect(invokedNode, equals(testNode));
      expect(result, isTrue);
      expect(invoked, isTrue);
      expect(invokedIntent, equals(intent));
    });
    testWidgets('Actions can invoke actions in ancestor dispatcher', (WidgetTester tester) async {
      final GlobalKey containerKey = GlobalKey();
      bool invoked = false;
      const Intent intent = Intent(TestAction.key);
      FocusNode passedNode;
      final FocusNode testNode = FocusNode(debugLabel: 'Test Node');
      final Action testAction = TestAction(
        onInvoke: (FocusNode node, Intent invocation) {
          invoked = true;
          passedNode = node;
        },
      );

      await tester.pumpWidget(
        Actions(
          dispatcher: TestDispatcher1(postInvoke: collect),
          actions: <LocalKey, ActionFactory>{
            TestAction.key: () => testAction,
          },
          child: Actions(
            dispatcher: TestDispatcher(postInvoke: collect),
            actions: const <LocalKey, ActionFactory>{},
            child: Container(key: containerKey),
          ),
        ),
      );

      await tester.pump();
      final bool result = Actions.invoke(
        containerKey.currentContext,
        intent,
        focusNode: testNode,
      );
      expect(passedNode, equals(testNode));
      expect(invokedNode, equals(testNode));
      expect(result, isTrue);
      expect(invoked, isTrue);
      expect(invokedIntent, equals(intent));
      expect(invokedAction, equals(testAction));
      expect(invokedDispatcher.runtimeType, equals(TestDispatcher1));
    });
    testWidgets("Actions can invoke actions in ancestor dispatcher if a lower one isn't specified", (WidgetTester tester) async {
      final GlobalKey containerKey = GlobalKey();
      bool invoked = false;
      const Intent intent = Intent(TestAction.key);
      FocusNode passedNode;
      final FocusNode testNode = FocusNode(debugLabel: 'Test Node');
      final Action testAction = TestAction(
        onInvoke: (FocusNode node, Intent invocation) {
          invoked = true;
          passedNode = node;
        },
      );

      await tester.pumpWidget(
        Actions(
          dispatcher: TestDispatcher1(postInvoke: collect),
          actions: <LocalKey, ActionFactory>{
            TestAction.key: () => testAction,
          },
          child: Actions(
            actions: const <LocalKey, ActionFactory>{},
            child: Container(key: containerKey),
          ),
        ),
      );

      await tester.pump();
      final bool result = Actions.invoke(
        containerKey.currentContext,
        intent,
        focusNode: testNode,
      );
      expect(passedNode, equals(testNode));
      expect(invokedNode, equals(testNode));
      expect(result, isTrue);
      expect(invoked, isTrue);
      expect(invokedIntent, equals(intent));
      expect(invokedAction, equals(testAction));
      expect(invokedDispatcher.runtimeType, equals(TestDispatcher1));
    });
    testWidgets('Actions widget can be found with of', (WidgetTester tester) async {
      final GlobalKey containerKey = GlobalKey();
      final ActionDispatcher testDispatcher = TestDispatcher1(postInvoke: collect);

      await tester.pumpWidget(
        Actions(
          dispatcher: testDispatcher,
          actions: const <LocalKey, ActionFactory>{},
          child: Container(key: containerKey),
        ),
      );

      await tester.pump();
      final ActionDispatcher dispatcher = Actions.of(
        containerKey.currentContext,
        nullOk: true,
      );
      expect(dispatcher, equals(testDispatcher));
    });
    testWidgets('FocusableActionDetector keeps track of focus and hover even when disabled.', (WidgetTester tester) async {
      FocusManager.instance.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
      final GlobalKey containerKey = GlobalKey();
      bool invoked = false;
      const Intent intent = Intent(TestAction.key);
      final FocusNode focusNode = FocusNode(debugLabel: 'Test Node');
      final Action testAction = TestAction(
        onInvoke: (FocusNode node, Intent invocation) {
          invoked = true;
        },
      );
      bool hovering = false;
      bool focusing = false;

      Future<void> buildTest(bool enabled) async {
        await tester.pumpWidget(
          Center(
            child: Actions(
              dispatcher: TestDispatcher1(postInvoke: collect),
              actions: const <LocalKey, ActionFactory>{},
              child: FocusableActionDetector(
                enabled: enabled,
                focusNode: focusNode,
                shortcuts: <LogicalKeySet, Intent>{
                  LogicalKeySet(LogicalKeyboardKey.enter): intent,
                },
                actions: <LocalKey, ActionFactory>{
                  TestAction.key: () => testAction,
                },
                onShowHoverHighlight: (bool value) => hovering = value,
                onShowFocusHighlight: (bool value) => focusing = value,
                child: Container(width: 100, height: 100, key: containerKey),
              ),
            ),
          ),
        );
        return tester.pump();
      }
      await buildTest(true);
      focusNode.requestFocus();
      await tester.pump();
      final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
      addTearDown(gesture.removePointer);
      await gesture.moveTo(tester.getCenter(find.byKey(containerKey)));
      await tester.pump();
      await tester.sendKeyEvent(LogicalKeyboardKey.enter);
      expect(hovering, isTrue);
      expect(focusing, isTrue);
      expect(invoked, isTrue);

      invoked = false;
      await buildTest(false);
      expect(hovering, isFalse);
      expect(focusing, isFalse);
      await tester.sendKeyEvent(LogicalKeyboardKey.enter);
      await tester.pump();
      expect(invoked, isFalse);
      await buildTest(true);
      expect(focusing, isFalse);
      expect(hovering, isTrue);
      await buildTest(false);
      expect(focusing, isFalse);
      expect(hovering, isFalse);
      await gesture.moveTo(Offset.zero);
      await buildTest(true);
      expect(hovering, isFalse);
      expect(focusing, isFalse);
    });
  });
  group('Diagnostics', () {
    testWidgets('default Intent debugFillProperties', (WidgetTester tester) async {
      final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();

      const Intent(ValueKey<String>('foo')).debugFillProperties(builder);

      final List<String> description = builder.properties
        .where((DiagnosticsNode node) {
          return !node.isFiltered(DiagnosticLevel.info);
        })
        .map((DiagnosticsNode node) => node.toString())
        .toList();

      expect(description, equals(<String>['key: [<\'foo\'>]']));
    });
    testWidgets('CallbackAction debugFillProperties', (WidgetTester tester) async {
      final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();

      CallbackAction(
        const ValueKey<String>('foo'),
        onInvoke: (FocusNode node, Intent intent) {},
      ).debugFillProperties(builder);

      final List<String> description = builder.properties
          .where((DiagnosticsNode node) {
            return !node.isFiltered(DiagnosticLevel.info);
          })
          .map((DiagnosticsNode node) => node.toString())
          .toList();

      expect(description, equals(<String>['intentKey: [<\'foo\'>]']));
    });
    testWidgets('default Actions debugFillProperties', (WidgetTester tester) async {
      final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();

      Actions(
        actions: const <LocalKey, ActionFactory>{},
        dispatcher: const ActionDispatcher(),
        child: Container(),
      ).debugFillProperties(builder);

      final List<String> description = builder.properties
        .where((DiagnosticsNode node) {
          return !node.isFiltered(DiagnosticLevel.info);
        })
        .map((DiagnosticsNode node) => node.toString())
        .toList();

      expect(description[0], equalsIgnoringHashCodes('dispatcher: ActionDispatcher#00000'));
      expect(description[1], equals('actions: {}'));
    });
    testWidgets('Actions implements debugFillProperties', (WidgetTester tester) async {
      final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();

      Actions(
        key: const ValueKey<String>('foo'),
        dispatcher: const ActionDispatcher(),
        actions: <LocalKey, ActionFactory>{
          const ValueKey<String>('bar'): () => TestAction(onInvoke: (FocusNode node, Intent intent) {}),
        },
        child: Container(key: const ValueKey<String>('baz')),
      ).debugFillProperties(builder);

      final List<String> description = builder.properties
          .where((DiagnosticsNode node) {
            return !node.isFiltered(DiagnosticLevel.info);
          })
          .map((DiagnosticsNode node) => node.toString())
          .toList();

      expect(description[0], equalsIgnoringHashCodes('dispatcher: ActionDispatcher#00000'));
      expect(description[1], equals('actions: {[<\'bar\'>]: Closure: () => TestAction}'));
    }, skip: isBrowser);
  });
}
