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

class _TestSliverPersistentHeaderDelegate extends SliverPersistentHeaderDelegate {
  _TestSliverPersistentHeaderDelegate({
    required this.minExtent,
    required this.maxExtent,
    required this.child,
    this.vsync = const TestVSync(),
    this.showOnScreenConfiguration = const PersistentHeaderShowOnScreenConfiguration(),
  });

  final Widget child;

  @override
  final double maxExtent;

  @override
  final double minExtent;

  @override
  final TickerProvider? vsync;

  @override
  final PersistentHeaderShowOnScreenConfiguration showOnScreenConfiguration;

  @override
  Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) => child;

  @override
  bool shouldRebuild(_TestSliverPersistentHeaderDelegate oldDelegate) => true;
}

void main() {
  const TextStyle textStyle = TextStyle();
  const Color cursorColor = Color.fromARGB(0xFF, 0xFF, 0x00, 0x00);

  testWidgets('tapping on a partly visible editable brings it fully on screen', (WidgetTester tester) async {
    final ScrollController scrollController = ScrollController();
    final TextEditingController controller = TextEditingController();
    final FocusNode focusNode = FocusNode();

    await tester.pumpWidget(MaterialApp(
      home: Center(
        child: Container(
          height: 300.0,
          child: ListView(
            controller: scrollController,
            children: <Widget>[
              EditableText(
                backgroundCursorColor: Colors.grey,
                controller: controller,
                focusNode: focusNode,
                style: textStyle,
                cursorColor: cursorColor,
              ),
              Container(
                height: 350.0,
              ),
            ],
          ),
        ),
      ),
    ));

    // Scroll the EditableText half off screen.
    final RenderBox render = tester.renderObject(find.byType(EditableText));
    scrollController.jumpTo(render.size.height / 2);
    await tester.pumpAndSettle();
    expect(scrollController.offset, render.size.height / 2);

    await tester.showKeyboard(find.byType(EditableText));
    await tester.pumpAndSettle();
    expect(scrollController.offset, 0.0);
  });

  testWidgets('tapping on a partly visible editable brings it fully on screen with scrollInsets', (WidgetTester tester) async {
    final ScrollController scrollController = ScrollController();
    final TextEditingController controller = TextEditingController();
    final FocusNode focusNode = FocusNode();

    await tester.pumpWidget(MaterialApp(
      home: Center(
        child: Container(
          height: 300.0,
          child: ListView(
            controller: scrollController,
            children: <Widget>[
              Container(
                height: 200.0,
              ),
              EditableText(
                backgroundCursorColor: Colors.grey,
                scrollPadding: const EdgeInsets.all(50.0),
                controller: controller,
                focusNode: focusNode,
                style: textStyle,
                cursorColor: cursorColor,
              ),
              Container(
                height: 850.0,
              ),
            ],
          ),
        ),
      ),
    ));

    // Scroll the EditableText half off screen.
    final RenderBox render = tester.renderObject(find.byType(EditableText));
    scrollController.jumpTo(200 + render.size.height / 2);
    await tester.pumpAndSettle();
    expect(scrollController.offset, 200 + render.size.height / 2);

    await tester.showKeyboard(find.byType(EditableText));
    await tester.pumpAndSettle();
    // Container above the text is 200 in height, the scrollInsets are 50
    // Tolerance of 5 units (The actual value was 152.0 in the current tests instead of 150.0)
    expect(scrollController.offset, lessThan(200.0 - 50.0 + 5.0));
    expect(scrollController.offset, greaterThan(200.0 - 50.0 - 5.0));
  });

  testWidgets('editable comes back on screen when entering text while it is off-screen', (WidgetTester tester) async {
    final ScrollController scrollController = ScrollController(initialScrollOffset: 100.0);
    final TextEditingController controller = TextEditingController();
    final FocusNode focusNode = FocusNode();

    await tester.pumpWidget(MaterialApp(
      home: Center(
        child: Container(
          height: 300.0,
          child: ListView(
            controller: scrollController,
            children: <Widget>[
              Container(
                height: 350.0,
              ),
              EditableText(
                backgroundCursorColor: Colors.grey,
                controller: controller,
                focusNode: focusNode,
                style: textStyle,
                cursorColor: cursorColor,
              ),
              Container(
                height: 350.0,
              ),
            ],
          ),
        ),
      ),
    ));

    // Focus the EditableText and scroll it off screen.
    await tester.showKeyboard(find.byType(EditableText));
    await tester.pumpAndSettle();
    expect(focusNode.hasFocus, isTrue);
    scrollController.jumpTo(0.0);
    await tester.pumpAndSettle();
    expect(scrollController.offset, 0.0);
    expect(find.byType(EditableText), findsNothing);

    // Entering text brings it back on screen.
    tester.testTextInput.enterText('Hello');
    await tester.pumpAndSettle();
    expect(scrollController.offset, greaterThan(0.0));
    expect(find.byType(EditableText), findsOneWidget);
  });

  testWidgets('entering text does not scroll when scrollPhysics.allowImplicitScrolling = false', (WidgetTester tester) async {
    // regression test for https://github.com/flutter/flutter/issues/19523

    final ScrollController scrollController = ScrollController(initialScrollOffset: 100.0);
    final TextEditingController controller = TextEditingController();
    final FocusNode focusNode = FocusNode();

    await tester.pumpWidget(MaterialApp(
      home: Center(
        child: Container(
          height: 300.0,
          child: ListView(
            physics: const NoImplicitScrollPhysics(),
            controller: scrollController,
            children: <Widget>[
              Container(
                height: 350.0,
              ),
              EditableText(
                backgroundCursorColor: Colors.grey,
                controller: controller,
                focusNode: focusNode,
                style: textStyle,
                cursorColor: cursorColor,
              ),
              Container(
                height: 350.0,
              ),
            ],
          ),
        ),
      ),
    ));

    // Focus the EditableText and scroll it off screen.
    await tester.showKeyboard(find.byType(EditableText));
    await tester.pumpAndSettle();
    expect(focusNode.hasFocus, isTrue);
    scrollController.jumpTo(0.0);
    await tester.pumpAndSettle();
    expect(scrollController.offset, 0.0);
    expect(find.byType(EditableText), findsNothing);

    // Entering text brings it not back on screen.
    tester.testTextInput.enterText('Hello');
    await tester.pumpAndSettle();
    expect(scrollController.offset, 0.0);
    expect(find.byType(EditableText), findsNothing);
  });

  testWidgets('entering text does not scroll a surrounding PageView', (WidgetTester tester) async {
    // regression test for https://github.com/flutter/flutter/issues/19523

    final TextEditingController textController = TextEditingController();
    final PageController pageController = PageController(initialPage: 1);

    await tester.pumpWidget(
      MaterialApp(
        home: MediaQuery(
          data: const MediaQueryData(devicePixelRatio: 1.0),
          child: Directionality(
            textDirection: TextDirection.ltr,
            child: Material(
              child: PageView(
                controller: pageController,
                children: <Widget>[
                  Container(
                    color: Colors.red,
                  ),
                  Container(
                    child: TextField(
                      controller: textController,
                    ),
                    color: Colors.green,
                  ),
                  Container(
                    color: Colors.red,
                  ),
                ],
              ),
            ),
          ),
        ),
      ),
    );

    await tester.showKeyboard(find.byType(EditableText));
    await tester.pumpAndSettle();
    expect(textController.text, '');
    tester.testTextInput.enterText('H');
    final int frames = await tester.pumpAndSettle();

    // The text input should not trigger any animations, which would indicate
    // that the surrounding PageView is incorrectly scrolling back-and-forth.
    expect(frames, 1);

    expect(textController.text, 'H');
  });

  testWidgets('focused multi-line editable scrolls caret back into view when typing', (WidgetTester tester) async {
    final ScrollController scrollController = ScrollController();
    final TextEditingController controller = TextEditingController();
    final FocusNode focusNode = FocusNode();
    controller.text = 'Start\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nEnd';

    await tester.pumpWidget(MaterialApp(
      home: Center(
        child: Container(
          height: 300.0,
          child: ListView(
            controller: scrollController,
            children: <Widget>[
              EditableText(
                backgroundCursorColor: Colors.grey,
                maxLines: null, // multiline
                controller: controller,
                focusNode: focusNode,
                style: textStyle,
                cursorColor: cursorColor,
              ),
            ],
          ),
        ),
      ),
    ));

    // Bring keyboard up and verify that end of EditableText is not on screen.
    await tester.showKeyboard(find.byType(EditableText));
    await tester.pumpAndSettle();
    scrollController.jumpTo(0.0);
    await tester.pumpAndSettle();
    final RenderBox render = tester.renderObject(find.byType(EditableText));
    expect(render.size.height, greaterThan(500.0));
    expect(scrollController.offset, 0.0);

    // Enter text at end, which is off-screen.
    final String textToEnter = '${controller.text} HELLO';
    tester.testTextInput.updateEditingValue(TextEditingValue(
      text: textToEnter,
      selection: TextSelection.collapsed(offset: textToEnter.length),
    ));
    await tester.pumpAndSettle();

    // Caret scrolls into view.
    expect(find.byType(EditableText), findsOneWidget);
    expect(render.size.height, greaterThan(500.0));
    expect(scrollController.offset, greaterThan(0.0));
  });

  testWidgets('scrolls into view with scrollInserts after the keyboard pops up', (WidgetTester tester) async {
    final ScrollController scrollController = ScrollController();
    final TextEditingController controller = TextEditingController();
    final FocusNode focusNode = FocusNode();

    const Key container = Key('container');

    await tester.pumpWidget(MaterialApp(
      home: Align(
        alignment: Alignment.bottomCenter,
        child: Container(
          height: 300.0,
          child: ListView(
            controller: scrollController,
            children: <Widget>[
              Container(
                key: container,
                height: 200.0,
              ),
              EditableText(
                backgroundCursorColor: Colors.grey,
                scrollPadding: const EdgeInsets.only(bottom: 300.0),
                controller: controller,
                focusNode: focusNode,
                style: textStyle,
                cursorColor: cursorColor,
              ),
              Container(
                height: 400.0,
              ),
            ],
          ),
        ),
      ),
    ));

    expect(scrollController.offset, 0.0);

    await tester.showKeyboard(find.byType(EditableText));
    await tester.pumpAndSettle();
    expect(scrollController.offset, greaterThan(0.0));
    expect(find.byKey(container), findsNothing);
  });

  testWidgets(
    'A pinned persistent header should not scroll when its descendant EditableText gains focus',
    (WidgetTester tester) async {
      // Regression test for https://github.com/flutter/flutter/issues/25507.
      ScrollController controller;
      final TextEditingController textEditingController = TextEditingController();
      final FocusNode focusNode = FocusNode();

      const Key headerKey = Key('header');
      await tester.pumpWidget(
        MaterialApp(
          home: Center(
            child: SizedBox(
              height: 600.0,
              width: 600.0,
              child: CustomScrollView(
                controller: controller = ScrollController(initialScrollOffset: 0),
                slivers: List<Widget>.generate(50, (int i) {
                  return i == 10
                  ? SliverPersistentHeader(
                    pinned: true,
                    floating: false,
                    delegate: _TestSliverPersistentHeaderDelegate(
                      minExtent: 50,
                      maxExtent: 50,
                      child: Container(
                        alignment: Alignment.topCenter,
                        child: EditableText(
                          key: headerKey,
                          backgroundCursorColor: Colors.grey,
                          controller: textEditingController,
                          focusNode: focusNode,
                          style: textStyle,
                          cursorColor: cursorColor,
                        ),
                      ),
                    ),
                  )
                  : SliverToBoxAdapter(
                    child: Container(
                      height: 100.0,
                      child: Text('Tile $i'),
                    ),
                  );
                }),
              ),
            ),
          ),
        ),
      );

      // The persistent header should now be pinned at the top.
      controller.jumpTo(100.0 * 15);
      await tester.pumpAndSettle();
      expect(controller.offset, 100.0 * 15);

      focusNode.requestFocus();
      await tester.pumpAndSettle();
      // The scroll offset should remain the same.
      expect(controller.offset, 100.0 * 15);
  });

  testWidgets(
    'A pinned persistent header should not scroll when its descendant EditableText gains focus (no animation)',
    (WidgetTester tester) async {
      // Regression test for https://github.com/flutter/flutter/issues/25507.
      ScrollController controller;
      final TextEditingController textEditingController = TextEditingController();
      final FocusNode focusNode = FocusNode();

      const Key headerKey = Key('header');
      await tester.pumpWidget(
        MaterialApp(
          home: Center(
            child: SizedBox(
              height: 600.0,
              width: 600.0,
              child: CustomScrollView(
                controller: controller = ScrollController(initialScrollOffset: 0),
                slivers: List<Widget>.generate(50, (int i) {
                  return i == 10
                    ? SliverPersistentHeader(
                      pinned: true,
                      floating: false,
                      delegate: _TestSliverPersistentHeaderDelegate(
                        minExtent: 50,
                        maxExtent: 50,
                        vsync: null,
                        child: Container(
                          alignment: Alignment.topCenter,
                          child: EditableText(
                            key: headerKey,
                            backgroundCursorColor: Colors.grey,
                            controller: textEditingController,
                            focusNode: focusNode,
                            style: textStyle,
                            cursorColor: cursorColor,
                          ),
                        ),
                      ),
                    )
                    : SliverToBoxAdapter(
                      child: Container(
                        height: 100.0,
                        child: Text('Tile $i'),
                      ),
                    );
                }),
              ),
            ),
          ),
        ),
      );

      // The persistent header should now be pinned at the top.
      controller.jumpTo(100.0 * 15);
      await tester.pumpAndSettle();
      expect(controller.offset, 100.0 * 15);

      focusNode.requestFocus();
      await tester.pumpAndSettle();
      // The scroll offset should remain the same.
      expect(controller.offset, 100.0 * 15);
  });
}

class NoImplicitScrollPhysics extends AlwaysScrollableScrollPhysics {
  const NoImplicitScrollPhysics({ ScrollPhysics? parent }) : super(parent: parent);

  @override
  bool get allowImplicitScrolling => false;

  @override
  NoImplicitScrollPhysics applyTo(ScrollPhysics? ancestor) {
    return NoImplicitScrollPhysics(parent: buildParent(ancestor));
  }
}
