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

import 'clipboard_utils.dart';
import 'editable_text_utils.dart';

void main() {
  final MockClipboard mockClipboard = MockClipboard();
  TestWidgetsFlutterBinding.ensureInitialized()
    .defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform, mockClipboard.handleMethodCall);

  setUp(() async {
    // Fill the clipboard so that the Paste option is available in the text
    // selection menu.
    await Clipboard.setData(const ClipboardData(text: 'Clipboard data'));
  });

  testWidgets('Hides and shows only a single menu', (WidgetTester tester) async {
    final GlobalKey key1 = GlobalKey();
    final GlobalKey key2 = GlobalKey();
    late final BuildContext context;

    await tester.pumpWidget(
      MaterialApp(
        home: Scaffold(
          body: Builder(
            builder: (BuildContext localContext) {
              context = localContext;
              return const SizedBox.shrink();
            },
          ),
        ),
      ),
    );

    expect(find.byKey(key1), findsNothing);
    expect(find.byKey(key2), findsNothing);

    final ContextMenuController controller1 = ContextMenuController();
    await tester.pump();
    expect(find.byKey(key1), findsNothing);
    expect(find.byKey(key2), findsNothing);

    controller1.show(
      context: context,
      contextMenuBuilder: (BuildContext context) {
        return Placeholder(key: key1);
      },
    );
    await tester.pump();

    expect(find.byKey(key1), findsOneWidget);
    expect(find.byKey(key2), findsNothing);

    // Showing the same thing again does nothing and is not an error.
    controller1.show(
      context: context,
      contextMenuBuilder: (BuildContext context) {
        return Placeholder(key: key1);
      },
    );
    await tester.pump();

    expect(tester.takeException(), null);
    expect(find.byKey(key1), findsOneWidget);
    expect(find.byKey(key2), findsNothing);

    // Showing a new menu hides the first.
    final ContextMenuController controller2 = ContextMenuController();
    controller2.show(
      context: context,
      contextMenuBuilder: (BuildContext context) {
        return Placeholder(key: key2);
      },
    );
    await tester.pump();

    expect(find.byKey(key1), findsNothing);
    expect(find.byKey(key2), findsOneWidget);

    controller2.remove();
    await tester.pump();

    expect(find.byKey(key1), findsNothing);
    expect(find.byKey(key2), findsNothing);
  });

  testWidgets('A menu can be hidden and then reshown', (WidgetTester tester) async {
    final GlobalKey key1 = GlobalKey();
    late final BuildContext context;

    await tester.pumpWidget(
      MaterialApp(
        home: Scaffold(
          body: Builder(
            builder: (BuildContext localContext) {
              context = localContext;
              return const SizedBox.shrink();
            },
          ),
        ),
      ),
    );

    expect(find.byKey(key1), findsNothing);

    final ContextMenuController controller = ContextMenuController();

    // Instantiating the controller does not shown it.
    await tester.pump();
    expect(find.byKey(key1), findsNothing);

    controller.show(
      context: context,
      contextMenuBuilder: (BuildContext context) {
        return Placeholder(key: key1);
      },
    );
    await tester.pump();

    expect(find.byKey(key1), findsOneWidget);

    controller.remove();
    await tester.pump();

    expect(find.byKey(key1), findsNothing);

    controller.show(
      context: context,
      contextMenuBuilder: (BuildContext context) {
        return Placeholder(key: key1);
      },
    );
    await tester.pump();

    expect(find.byKey(key1), findsOneWidget);
  });

  testWidgets('markNeedsBuild causes the builder to update', (WidgetTester tester) async {
    int buildCount = 0;
    late final BuildContext context;

    await tester.pumpWidget(
      MaterialApp(
        home: Scaffold(
          body: Builder(
            builder: (BuildContext localContext) {
              context = localContext;
              return const SizedBox.shrink();
            },
          ),
        ),
      ),
    );

    final ContextMenuController controller = ContextMenuController();
    controller.show(
      context: context,
      contextMenuBuilder: (BuildContext context) {
        buildCount++;
        return const Placeholder();
      },
    );
    expect(buildCount, 0);
    await tester.pump();
    expect(buildCount, 1);

    controller.markNeedsBuild();
    expect(buildCount, 1);
    await tester.pump();
    expect(buildCount, 2);

    controller.remove();
  });

  testWidgets('Calling show when a built-in widget is already showing its context menu hides the built-in menu', (WidgetTester tester) async {
    final GlobalKey builtInKey = GlobalKey();
    final GlobalKey directKey = GlobalKey();
    late final BuildContext context;

    final TextEditingController textEditingController = TextEditingController();
    addTearDown(textEditingController.dispose);

    final FocusNode focusNode = FocusNode();
    addTearDown(focusNode.dispose);

    await tester.pumpWidget(
      MaterialApp(
        home: Scaffold(
          body: Builder(
            builder: (BuildContext localContext) {
              context = localContext;
              return EditableText(
                controller: textEditingController,
                backgroundCursorColor: Colors.grey,
                focusNode: focusNode,
                style: const TextStyle(),
                cursorColor: Colors.red,
                selectionControls: materialTextSelectionHandleControls,
                contextMenuBuilder: (
                  BuildContext context,
                  EditableTextState editableTextState,
                ) {
                  return Placeholder(key: builtInKey);
                },
              );
            },
          ),
        ),
      ),
    );

    expect(find.byKey(builtInKey), findsNothing);
    expect(find.byKey(directKey), findsNothing);

    final EditableTextState state = tester.state<EditableTextState>(find.byType(EditableText));

    await tester.tapAt(textOffsetToPosition(tester, 0));
    await tester.pump();
    expect(state.showToolbar(), true);
    await tester.pump();

    expect(find.byKey(builtInKey), findsOneWidget);
    expect(find.byKey(directKey), findsNothing);

    final ContextMenuController controller = ContextMenuController();
    controller.show(
      context: context,
      contextMenuBuilder: (BuildContext context) {
        return Placeholder(key: directKey);
      },
    );
    await tester.pump();

    expect(find.byKey(builtInKey), findsNothing);
    expect(find.byKey(directKey), findsOneWidget);
    expect(controller.isShown, isTrue);

    // And showing the built-in menu hides the directly shown menu.
    expect(state.showToolbar(), isTrue);
    await tester.pump();

    expect(find.byKey(builtInKey), findsOneWidget);
    expect(find.byKey(directKey), findsNothing);
    expect(controller.isShown, isFalse);

    // Calling remove on the hidden ContextMenuController does not hide the
    // built-in menu.
    controller.remove();
    await tester.pump();

    expect(find.byKey(builtInKey), findsOneWidget);
    expect(find.byKey(directKey), findsNothing);
    expect(controller.isShown, isFalse);

    state.hideToolbar();
    await tester.pump();
    expect(find.byKey(builtInKey), findsNothing);
    expect(find.byKey(directKey), findsNothing);
    expect(controller.isShown, isFalse);
  },
    skip: isContextMenuProvidedByPlatform, // [intended] no Flutter-drawn text selection toolbar on web.
  );
}
