// 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_test/flutter_test.dart';
import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart';
import '../widgets/editable_text_utils.dart' show textOffsetToPosition;

const double _kToolbarContentDistance = 8.0;

// A custom text selection menu that just displays a single custom button.
class _CustomMaterialTextSelectionControls extends MaterialTextSelectionControls {
  @override
  Widget buildToolbar(
    BuildContext context,
    Rect globalEditableRegion,
    double textLineHeight,
    Offset selectionMidpoint,
    List<TextSelectionPoint> endpoints,
    TextSelectionDelegate delegate,
    ValueListenable<ClipboardStatus>? clipboardStatus,
    Offset? lastSecondaryTapDownPosition,
  ) {
    final TextSelectionPoint startTextSelectionPoint = endpoints[0];
    final TextSelectionPoint endTextSelectionPoint = endpoints.length > 1
      ? endpoints[1]
      : endpoints[0];
    final Offset anchorAbove = Offset(
      globalEditableRegion.left + selectionMidpoint.dx,
      globalEditableRegion.top + startTextSelectionPoint.point.dy - textLineHeight - _kToolbarContentDistance,
    );
    final Offset anchorBelow = Offset(
      globalEditableRegion.left + selectionMidpoint.dx,
      globalEditableRegion.top + endTextSelectionPoint.point.dy + TextSelectionToolbar.kToolbarContentDistanceBelow,
    );

    return TextSelectionToolbar(
      anchorAbove: anchorAbove,
      anchorBelow: anchorBelow,
      children: <Widget>[
        TextSelectionToolbarTextButton(
          padding: TextSelectionToolbarTextButton.getPadding(0, 1),
          onPressed: () {},
          child: const Text('Custom button'),
        ),
      ],
    );
  }
}

class TestBox extends SizedBox {
  const TestBox({super.key}) : super(width: itemWidth, height: itemHeight);

  static const double itemHeight = 44.0;
  static const double itemWidth = 100.0;
}

void main() {
  TestWidgetsFlutterBinding.ensureInitialized();

  // Find by a runtimeType String, including private types.
  Finder findPrivate(String type) {
    return find.descendant(
      of: find.byType(MaterialApp),
      matching: find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == type),
    );
  }

  // Finding TextSelectionToolbar won't give you the position as the user sees
  // it because it's a full-sized Stack at the top level. This method finds the
  // visible part of the toolbar for use in measurements.
  Finder findToolbar() => findPrivate('_TextSelectionToolbarOverflowable');

  Finder findOverflowButton() => findPrivate('_TextSelectionToolbarOverflowButton');

  testWidgetsWithLeakTracking('puts children in an overflow menu if they overflow', (WidgetTester tester) async {
    late StateSetter setState;
    final List<Widget> children = List<Widget>.generate(7, (int i) => const TestBox());

    await tester.pumpWidget(
      MaterialApp(
        home: Scaffold(
          body: StatefulBuilder(
            builder: (BuildContext context, StateSetter setter) {
              setState = setter;
              return TextSelectionToolbar(
                anchorAbove: const Offset(50.0, 100.0),
                anchorBelow: const Offset(50.0, 200.0),
                children: children,
              );
            },
          ),
        ),
      ),
    );

    // All children fit on the screen, so they are all rendered.
    expect(find.byType(TestBox), findsNWidgets(children.length));
    expect(findOverflowButton(), findsNothing);

    // Adding one more child makes the children overflow.
    setState(() {
      children.add(
        const TestBox(),
      );
    });
    await tester.pumpAndSettle();
    expect(find.byType(TestBox), findsNWidgets(children.length - 1));
    expect(findOverflowButton(), findsOneWidget);

    // Tap the overflow button to show the overflow menu.
    await tester.tap(findOverflowButton());
    await tester.pumpAndSettle();
    expect(find.byType(TestBox), findsNWidgets(1));
    expect(findOverflowButton(), findsOneWidget);

    // Tap the overflow button again to hide the overflow menu.
    await tester.tap(findOverflowButton());
    await tester.pumpAndSettle();
    expect(find.byType(TestBox), findsNWidgets(children.length - 1));
    expect(findOverflowButton(), findsOneWidget);
  });

  testWidgets('positions itself at anchorAbove if it fits', (WidgetTester tester) async {
    late StateSetter setState;
    const double height = 44.0;
    const double anchorBelowY = 500.0;
    double anchorAboveY = 0.0;

    await tester.pumpWidget(
      MaterialApp(
        home: Scaffold(
          body: StatefulBuilder(
            builder: (BuildContext context, StateSetter setter) {
              setState = setter;
              return TextSelectionToolbar(
                anchorAbove: Offset(50.0, anchorAboveY),
                anchorBelow: const Offset(50.0, anchorBelowY),
                children: <Widget>[
                  Container(color: Colors.red, width: 50.0, height: height),
                  Container(color: Colors.green, width: 50.0, height: height),
                  Container(color: Colors.blue, width: 50.0, height: height),
                ],
              );
            },
          ),
        ),
      ),
    );

    // When the toolbar doesn't fit above aboveAnchor, it positions itself below
    // belowAnchor.
    double toolbarY = tester.getTopLeft(findToolbar()).dy;
    expect(toolbarY, equals(anchorBelowY + TextSelectionToolbar.kToolbarContentDistanceBelow));

    // Even when it barely doesn't fit.
    setState(() {
      anchorAboveY = 60.0;
    });
    await tester.pump();
    toolbarY = tester.getTopLeft(findToolbar()).dy;
    expect(toolbarY, equals(anchorBelowY + TextSelectionToolbar.kToolbarContentDistanceBelow));

    // When it does fit above aboveAnchor, it positions itself there.
    setState(() {
      anchorAboveY = 70.0;
    });
    await tester.pump();
    toolbarY = tester.getTopLeft(findToolbar()).dy;
    expect(toolbarY, equals(anchorAboveY - height - _kToolbarContentDistance));
  });

  testWidgets('can create and use a custom toolbar', (WidgetTester tester) async {
    await tester.pumpWidget(
      MaterialApp(
        home: Scaffold(
          body: Center(
            child: SelectableText(
              'Select me custom menu',
              selectionControls: _CustomMaterialTextSelectionControls(),
            ),
          ),
        ),
      ),
    );

    // The selection menu is not initially shown.
    expect(find.text('Custom button'), findsNothing);

    // Long press on "custom" to select it.
    final Offset customPos = textOffsetToPosition(tester, 11);
    final TestGesture gesture = await tester.startGesture(customPos, pointer: 7);
    await tester.pump(const Duration(seconds: 2));
    await gesture.up();
    await tester.pump();

    // The custom selection menu is shown.
    expect(find.text('Custom button'), findsOneWidget);
    expect(find.text('Cut'), findsNothing);
    expect(find.text('Copy'), findsNothing);
    expect(find.text('Paste'), findsNothing);
    expect(find.text('Select all'), findsNothing);
  }, skip: kIsWeb); // [intended] We don't show the toolbar on the web.

  for (final ColorScheme colorScheme in <ColorScheme>[ThemeData.light().colorScheme, ThemeData.dark().colorScheme]) {
    testWidgetsWithLeakTracking('default background color', (WidgetTester tester) async {
      await tester.pumpWidget(
        MaterialApp(
          theme: ThemeData(
            colorScheme: colorScheme,
          ),
          home: Scaffold(
            body: Center(
              child: TextSelectionToolbar(
                anchorAbove: Offset.zero,
                anchorBelow: Offset.zero,
                children: <Widget>[
                  TextSelectionToolbarTextButton(
                    padding: TextSelectionToolbarTextButton.getPadding(0, 1),
                    onPressed: () {},
                    child: const Text('Custom button'),
                  ),
                ],
              ),
            ),
          ),
        ),
      );

      Finder findToolbarContainer() {
        return find.descendant(
          of: find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_TextSelectionToolbarContainer'),
          matching: find.byType(Material),
        );
      }
      expect(findToolbarContainer(), findsAtLeastNWidgets(1));

      final Material toolbarContainer = tester.widget(findToolbarContainer().first);
      expect(
        toolbarContainer.color,
        // The default colors are hardcoded and don't take the default value of
        // the theme's surface color.
        switch (colorScheme.brightness) {
          Brightness.light => const Color(0xffffffff),
          Brightness.dark => const Color(0xff424242),
        },
      );
    });

    testWidgetsWithLeakTracking('custom background color', (WidgetTester tester) async {
      const Color customBackgroundColor = Colors.red;

      await tester.pumpWidget(
        MaterialApp(
          theme: ThemeData(
            colorScheme: colorScheme.copyWith(
              surface: customBackgroundColor,
            ),
          ),
          home: Scaffold(
            body: Center(
              child: TextSelectionToolbar(
                anchorAbove: Offset.zero,
                anchorBelow: Offset.zero,
                children: <Widget>[
                  TextSelectionToolbarTextButton(
                    padding: TextSelectionToolbarTextButton.getPadding(0, 1),
                    onPressed: () {},
                    child: const Text('Custom button'),
                  ),
                ],
              ),
            ),
          ),
        ),
      );

      Finder findToolbarContainer() {
        return find.descendant(
          of: find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_TextSelectionToolbarContainer'),
          matching: find.byType(Material),
        );
      }
      expect(findToolbarContainer(), findsAtLeastNWidgets(1));

      final Material toolbarContainer = tester.widget(findToolbarContainer().first);
      expect(
        toolbarContainer.color,
        customBackgroundColor,
      );
    });
  }
}