// 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/rendering.dart';
import 'package:flutter/services.dart';
import 'package:flutter/src/foundation/diagnostics.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
  TestWidgetsFlutterBinding.ensureInitialized();

  late FakeMenuChannel fakeMenuChannel;
  late PlatformMenuDelegate originalDelegate;
  late DefaultPlatformMenuDelegate delegate;
  final List<String> selected = <String>[];
  final List<String> opened = <String>[];
  final List<String> closed = <String>[];

  void onSelected(String item) {
    selected.add(item);
  }

  void onOpen(String item) {
    opened.add(item);
  }

  void onClose(String item) {
    closed.add(item);
  }

  setUp(() {
    fakeMenuChannel = FakeMenuChannel((MethodCall call) async {});
    delegate = DefaultPlatformMenuDelegate(channel: fakeMenuChannel);
    originalDelegate = WidgetsBinding.instance.platformMenuDelegate;
    WidgetsBinding.instance.platformMenuDelegate = delegate;
    selected.clear();
    opened.clear();
    closed.clear();
  });

  tearDown(() {
    WidgetsBinding.instance.platformMenuDelegate = originalDelegate;
  });

  group('PlatformMenuBar', () {
    group('basic menu structure is transmitted to platform', () {
      testWidgets('using onSelected', (WidgetTester tester) async {
        await tester.pumpWidget(
          MaterialApp(
            home: Material(
              child: PlatformMenuBar(
                menus: createTestMenus(
                  onSelected: onSelected,
                  onOpen: onOpen,
                  onClose: onClose,
                  shortcuts: <String, MenuSerializableShortcut>{
                    subSubMenu10[0]: const SingleActivator(LogicalKeyboardKey.keyA, control: true),
                    subSubMenu10[1]: const SingleActivator(LogicalKeyboardKey.keyB, shift: true),
                    subSubMenu10[2]: const SingleActivator(LogicalKeyboardKey.keyC, alt: true),
                    subSubMenu10[3]: const SingleActivator(LogicalKeyboardKey.keyD, meta: true),
                  },
                ),
                child: const Center(child: Text('Body')),
              ),
            ),
          ),
        );

        expect(
          fakeMenuChannel.outgoingCalls.last.method,
          equals('Menu.setMenus'),
        );
        expect(
          fakeMenuChannel.outgoingCalls.last.arguments,
          equals(expectedStructure),
        );
      });
      testWidgets('using onSelectedIntent', (WidgetTester tester) async {
        await tester.pumpWidget(
          MaterialApp(
            home: Material(
              child: PlatformMenuBar(
                menus: createTestMenus(
                  onSelectedIntent: const DoNothingIntent(),
                  onOpen: onOpen,
                  onClose: onClose,
                  shortcuts: <String, MenuSerializableShortcut>{
                    subSubMenu10[0]: const SingleActivator(LogicalKeyboardKey.keyA, control: true),
                    subSubMenu10[1]: const SingleActivator(LogicalKeyboardKey.keyB, shift: true),
                    subSubMenu10[2]: const SingleActivator(LogicalKeyboardKey.keyC, alt: true),
                    subSubMenu10[3]: const SingleActivator(LogicalKeyboardKey.keyD, meta: true),
                  },
                ),
                child: const Center(child: Text('Body')),
              ),
            ),
          ),
        );

        expect(
          fakeMenuChannel.outgoingCalls.last.method,
          equals('Menu.setMenus'),
        );
        expect(
          fakeMenuChannel.outgoingCalls.last.arguments,
          equals(expectedStructure),
        );
      });
    });
    testWidgets('asserts when more than one has locked the delegate', (WidgetTester tester) async {
      await tester.pumpWidget(
        const MaterialApp(
          home: Material(
            child: PlatformMenuBar(
              menus: <PlatformMenuItem>[],
              child: PlatformMenuBar(
                menus: <PlatformMenuItem>[],
                child: SizedBox(),
              ),
            ),
          ),
        ),
      );
      expect(tester.takeException(), isA<AssertionError>());
    });
    testWidgets('diagnostics', (WidgetTester tester) async {
      const PlatformMenuItem item = PlatformMenuItem(
        label: 'label2',
        shortcut: SingleActivator(LogicalKeyboardKey.keyA),
      );
      const PlatformMenuBar menuBar = PlatformMenuBar(
        menus: <PlatformMenuItem>[item],
        child: SizedBox(),
      );

      await tester.pumpWidget(
        const MaterialApp(
          home: Material(
            child: menuBar,
          ),
        ),
      );
      await tester.pump();

      expect(
        menuBar.toStringDeep(),
        equalsIgnoringHashCodes(
          'PlatformMenuBar#00000\n'
          ' └─PlatformMenuItem#00000(label2)\n'
          '     label: "label2"\n'
          '     shortcut: SingleActivator#00000(keys: Key A)\n'
          '     DISABLED\n',
        ),
      );
    });
  });
  group('MenuBarItem', () {
    testWidgets('diagnostics', (WidgetTester tester) async {
      const PlatformMenuItem childItem = PlatformMenuItem(
        label: 'label',
      );
      const PlatformMenu item = PlatformMenu(
        label: 'label',
        menus: <PlatformMenuItem>[childItem],
      );

      final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
      item.debugFillProperties(builder);

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

      expect(description, <String>[
        'label: "label"',
      ]);
    });
  });

  group('ShortcutSerialization', () {
    testWidgets('character constructor', (WidgetTester tester) async {
      final ShortcutSerialization serialization = ShortcutSerialization.character('?');
      expect(serialization.toChannelRepresentation(), equals(<String, Object?>{
        'shortcutCharacter': '?',
        'shortcutModifiers': 0,
      }));
      final ShortcutSerialization serializationWithModifiers = ShortcutSerialization.character('?', alt: true, control: true, meta: true);
      expect(serializationWithModifiers.toChannelRepresentation(), equals(<String, Object?>{
        'shortcutCharacter': '?',
        'shortcutModifiers': 13,
      }));
    });

    testWidgets('modifier constructor', (WidgetTester tester) async {
      final ShortcutSerialization serialization = ShortcutSerialization.modifier(LogicalKeyboardKey.home);
      expect(serialization.toChannelRepresentation(), equals(<String, Object?>{
        'shortcutTrigger': LogicalKeyboardKey.home.keyId,
        'shortcutModifiers': 0,
      }));
      final ShortcutSerialization serializationWithModifiers = ShortcutSerialization.modifier(LogicalKeyboardKey.home, alt: true, control: true, meta: true, shift: true);
      expect(serializationWithModifiers.toChannelRepresentation(), equals(<String, Object?>{
        'shortcutTrigger': LogicalKeyboardKey.home.keyId,
        'shortcutModifiers': 15,
      }));
    });
  });
}

const List<String> mainMenu = <String>[
  'Menu 0',
  'Menu 1',
  'Menu 2',
  'Menu 3',
];

const List<String> subMenu0 = <String>[
  'Sub Menu 00',
];

const List<String> subMenu1 = <String>[
  'Sub Menu 10',
  'Sub Menu 11',
  'Sub Menu 12',
];

const List<String> subSubMenu10 = <String>[
  'Sub Sub Menu 110',
  'Sub Sub Menu 111',
  'Sub Sub Menu 112',
  'Sub Sub Menu 113',
];

const List<String> subMenu2 = <String>[
  'Sub Menu 20',
];

List<PlatformMenuItem> createTestMenus({
  void Function(String)? onSelected,
  Intent? onSelectedIntent,
  void Function(String)? onOpen,
  void Function(String)? onClose,
  Map<String, MenuSerializableShortcut> shortcuts = const <String, MenuSerializableShortcut>{},
  bool includeStandard = false,
}) {
  final List<PlatformMenuItem> result = <PlatformMenuItem>[
    PlatformMenu(
      label: mainMenu[0],
      onOpen: onOpen != null ? () => onOpen(mainMenu[0]) : null,
      onClose: onClose != null ? () => onClose(mainMenu[0]) : null,
      menus: <PlatformMenuItem>[
        PlatformMenuItem(
          label: subMenu0[0],
          onSelected: onSelected != null ? () => onSelected(subMenu0[0]) : null,
          onSelectedIntent: onSelectedIntent,
          shortcut: shortcuts[subMenu0[0]],
        ),
      ],
    ),
    PlatformMenu(
      label: mainMenu[1],
      onOpen: onOpen != null ? () => onOpen(mainMenu[1]) : null,
      onClose: onClose != null ? () => onClose(mainMenu[1]) : null,
      menus: <PlatformMenuItem>[
        PlatformMenuItemGroup(
          members: <PlatformMenuItem>[
            PlatformMenuItem(
              label: subMenu1[0],
              onSelected: onSelected != null ? () => onSelected(subMenu0[0]) : null,
              onSelectedIntent: onSelectedIntent,
              shortcut: shortcuts[subMenu1[0]],
            ),
          ],
        ),
        PlatformMenu(
          label: subMenu1[1],
          onOpen: onOpen != null ? () => onOpen(subMenu1[1]) : null,
          onClose: onClose != null ? () => onClose(subMenu1[1]) : null,
          menus: <PlatformMenuItem>[
            PlatformMenuItemGroup(
              members: <PlatformMenuItem>[
                PlatformMenuItem(
                  label: subSubMenu10[0],
                  onSelected: onSelected != null ? () => onSelected(subSubMenu10[0]) : null,
                  onSelectedIntent: onSelectedIntent,
                  shortcut: shortcuts[subSubMenu10[0]],
                ),
              ],
            ),
            PlatformMenuItemGroup(
              members: <PlatformMenuItem>[
                PlatformMenuItem(
                  label: subSubMenu10[1],
                  onSelected: onSelected != null ? () => onSelected(subSubMenu10[1]) : null,
                  onSelectedIntent: onSelectedIntent,
                  shortcut: shortcuts[subSubMenu10[1]],
                ),
              ],
            ),
            PlatformMenuItem(
              label: subSubMenu10[2],
              onSelected: onSelected != null ? () => onSelected(subSubMenu10[2]) : null,
              onSelectedIntent: onSelectedIntent,
              shortcut: shortcuts[subSubMenu10[2]],
            ),
            PlatformMenuItemGroup(
              members: <PlatformMenuItem>[
                PlatformMenuItem(
                  label: subSubMenu10[3],
                  onSelected: onSelected != null ? () => onSelected(subSubMenu10[3]) : null,
                  onSelectedIntent: onSelectedIntent,
                  shortcut: shortcuts[subSubMenu10[3]],
                ),
              ],
            ),
          ],
        ),
        PlatformMenuItem(
          label: subMenu1[2],
          onSelected: onSelected != null ? () => onSelected(subMenu1[2]) : null,
          onSelectedIntent: onSelectedIntent,
          shortcut: shortcuts[subMenu1[2]],
        ),
      ],
    ),
    PlatformMenu(
      label: mainMenu[2],
      onOpen: onOpen != null ? () => onOpen(mainMenu[2]) : null,
      onClose: onClose != null ? () => onClose(mainMenu[2]) : null,
      menus: <PlatformMenuItem>[
        PlatformMenuItem(
          // Always disabled.
          label: subMenu2[0],
          shortcut: shortcuts[subMenu2[0]],
        ),
      ],
    ),
    // Disabled menu
    PlatformMenu(
      label: mainMenu[3],
      onOpen: onOpen != null ? () => onOpen(mainMenu[2]) : null,
      onClose: onClose != null ? () => onClose(mainMenu[2]) : null,
      menus: <PlatformMenuItem>[],
    ),
  ];
  return result;
}

const Map<String, Object?> expectedStructure = <String, Object?>{
  '0': <Map<String, Object?>>[
    <String, Object?>{
      'id': 2,
      'label': 'Menu 0',
      'enabled': true,
      'children': <Map<String, Object?>>[
        <String, Object?>{
          'id': 1,
          'label': 'Sub Menu 00',
          'enabled': true,
        },
      ],
    },
    <String, Object?>{
      'id': 18,
      'label': 'Menu 1',
      'enabled': true,
      'children': <Map<String, Object?>>[
        <String, Object?>{
          'id': 4,
          'label': 'Sub Menu 10',
          'enabled': true,
        },
        <String, Object?>{'id': 5, 'isDivider': true},
        <String, Object?>{
          'id': 16,
          'label': 'Sub Menu 11',
          'enabled': true,
          'children': <Map<String, Object?>>[
            <String, Object?>{
              'id': 7,
              'label': 'Sub Sub Menu 110',
              'enabled': true,
              'shortcutTrigger': 97,
              'shortcutModifiers': 8,
            },
            <String, Object?>{'id': 8, 'isDivider': true},
            <String, Object?>{
              'id': 10,
              'label': 'Sub Sub Menu 111',
              'enabled': true,
              'shortcutTrigger': 98,
              'shortcutModifiers': 2,
            },
            <String, Object?>{'id': 11, 'isDivider': true},
            <String, Object?>{
              'id': 12,
              'label': 'Sub Sub Menu 112',
              'enabled': true,
              'shortcutTrigger': 99,
              'shortcutModifiers': 4,
            },
            <String, Object?>{'id': 13, 'isDivider': true},
            <String, Object?>{
              'id': 14,
              'label': 'Sub Sub Menu 113',
              'enabled': true,
              'shortcutTrigger': 100,
              'shortcutModifiers': 1,
            },
          ],
        },
        <String, Object?>{
          'id': 17,
          'label': 'Sub Menu 12',
          'enabled': true,
        },
      ],
    },
    <String, Object?>{
      'id': 20,
      'label': 'Menu 2',
      'enabled': true,
      'children': <Map<String, Object?>>[
        <String, Object?>{
          'id': 19,
          'label': 'Sub Menu 20',
          'enabled': false,
        },
      ],
    },
    <String, Object?>{'id': 21, 'label': 'Menu 3', 'enabled': false, 'children': <Map<String, Object?>>[]},
  ],
};

class FakeMenuChannel implements MethodChannel {
  FakeMenuChannel(this.outgoing);

  Future<dynamic> Function(MethodCall) outgoing;
  Future<void> Function(MethodCall)? incoming;

  List<MethodCall> outgoingCalls = <MethodCall>[];

  @override
  BinaryMessenger get binaryMessenger => throw UnimplementedError();

  @override
  MethodCodec get codec => const StandardMethodCodec();

  @override
  Future<List<T>> invokeListMethod<T>(String method, [dynamic arguments]) => throw UnimplementedError();

  @override
  Future<Map<K, V>> invokeMapMethod<K, V>(String method, [dynamic arguments]) => throw UnimplementedError();

  @override
  Future<T> invokeMethod<T>(String method, [dynamic arguments]) async {
    final MethodCall call = MethodCall(method, arguments);
    outgoingCalls.add(call);
    return await outgoing(call) as T;
  }

  @override
  String get name => 'flutter/menu';

  @override
  void setMethodCallHandler(Future<void> Function(MethodCall call)? handler) => incoming = handler;
}