// 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.

// @dart = 2.8

import 'dart:async';
import 'dart:ui';

import 'package:meta/meta.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/material.dart';

ScrollController _controller = ScrollController(
  initialScrollOffset: 110.0,
);

class ThePositiveNumbers extends StatelessWidget {
  const ThePositiveNumbers({
    Key key,
    @required this.from,
  }) : super(key: key);
  final int from;
  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      key: const PageStorageKey<String>('ThePositiveNumbers'),
      itemExtent: 100.0,
      controller: _controller,
      itemBuilder: (BuildContext context, int index) {
        return Text('${index + from}', key: ValueKey<int>(index));
      },
    );
  }
}

Future<void> performTest(WidgetTester tester, bool maintainState) async {
  final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
  await tester.pumpWidget(
    Directionality(
      textDirection: TextDirection.ltr,
      child: Navigator(
        key: navigatorKey,
        onGenerateRoute: (RouteSettings settings) {
          if (settings.name == '/') {
            return MaterialPageRoute<void>(
              settings: settings,
              builder: (_) => Container(child: const ThePositiveNumbers(from: 0)),
              maintainState: maintainState,
            );
          } else if (settings.name == '/second') {
            return MaterialPageRoute<void>(
              settings: settings,
              builder: (_) => Container(child: const ThePositiveNumbers(from: 10000)),
              maintainState: maintainState,
            );
          }
          return null;
        },
      ),
    ),
  );

  // we're 600 pixels high, each item is 100 pixels high, scroll position is
  // 110.0, so we should have 7 items, 1..7.
  expect(find.text('0'), findsNothing, reason: 'with maintainState: $maintainState');
  expect(find.text('1'), findsOneWidget, reason: 'with maintainState: $maintainState');
  expect(find.text('2'), findsOneWidget, reason: 'with maintainState: $maintainState');
  expect(find.text('3'), findsOneWidget, reason: 'with maintainState: $maintainState');
  expect(find.text('4'), findsOneWidget, reason: 'with maintainState: $maintainState');
  expect(find.text('5'), findsOneWidget, reason: 'with maintainState: $maintainState');
  expect(find.text('6'), findsOneWidget, reason: 'with maintainState: $maintainState');
  expect(find.text('7'), findsOneWidget, reason: 'with maintainState: $maintainState');
  expect(find.text('8'), findsNothing, reason: 'with maintainState: $maintainState');
  expect(find.text('10'), findsNothing, reason: 'with maintainState: $maintainState');
  expect(find.text('100'), findsNothing, reason: 'with maintainState: $maintainState');

  tester.state<ScrollableState>(find.byType(Scrollable)).position.jumpTo(1000.0);
  await tester.pump(const Duration(seconds: 1));

  // we're 600 pixels high, each item is 100 pixels high, scroll position is
  // 1000, so we should have exactly 6 items, 10..15.

  expect(find.text('0'), findsNothing, reason: 'with maintainState: $maintainState');
  expect(find.text('1'), findsNothing, reason: 'with maintainState: $maintainState');
  expect(find.text('8'), findsNothing, reason: 'with maintainState: $maintainState');
  expect(find.text('9'), findsNothing, reason: 'with maintainState: $maintainState');
  expect(find.text('10'), findsOneWidget, reason: 'with maintainState: $maintainState');
  expect(find.text('11'), findsOneWidget, reason: 'with maintainState: $maintainState');
  expect(find.text('12'), findsOneWidget, reason: 'with maintainState: $maintainState');
  expect(find.text('13'), findsOneWidget, reason: 'with maintainState: $maintainState');
  expect(find.text('14'), findsOneWidget, reason: 'with maintainState: $maintainState');
  expect(find.text('15'), findsOneWidget, reason: 'with maintainState: $maintainState');
  expect(find.text('16'), findsNothing, reason: 'with maintainState: $maintainState');
  expect(find.text('100'), findsNothing, reason: 'with maintainState: $maintainState');

  navigatorKey.currentState.pushNamed('/second');
  await tester.pump(); // navigating always takes two frames, one to start...
  await tester.pump(const Duration(seconds: 1)); // ...and one to end the transition

  // the second list is now visible, starting at 10001
  expect(find.text('0'), findsNothing, reason: 'with maintainState: $maintainState');
  expect(find.text('1'), findsNothing, reason: 'with maintainState: $maintainState');
  expect(find.text('10'), findsNothing, reason: 'with maintainState: $maintainState');
  expect(find.text('11'), findsNothing, reason: 'with maintainState: $maintainState');
  expect(find.text('10000'), findsNothing, reason: 'with maintainState: $maintainState');
  expect(find.text('10001'), findsOneWidget, reason: 'with maintainState: $maintainState');
  expect(find.text('10002'), findsOneWidget, reason: 'with maintainState: $maintainState');
  expect(find.text('10003'), findsOneWidget, reason: 'with maintainState: $maintainState');
  expect(find.text('10004'), findsOneWidget, reason: 'with maintainState: $maintainState');
  expect(find.text('10005'), findsOneWidget, reason: 'with maintainState: $maintainState');
  expect(find.text('10006'), findsOneWidget, reason: 'with maintainState: $maintainState');
  expect(find.text('10007'), findsOneWidget, reason: 'with maintainState: $maintainState');
  expect(find.text('10008'), findsNothing, reason: 'with maintainState: $maintainState');
  expect(find.text('10010'), findsNothing, reason: 'with maintainState: $maintainState');
  expect(find.text('10100'), findsNothing, reason: 'with maintainState: $maintainState');

  navigatorKey.currentState.pop();
  await tester.pump(); // again, navigating always takes two frames

  // Ensure we don't clamp the scroll offset even during the navigation.
  // https://github.com/flutter/flutter/issues/4883
  final ScrollableState state = tester.state(find.byType(Scrollable).first);
  expect(state.position.pixels, equals(1000.0), reason: 'with maintainState: $maintainState');

  await tester.pump(const Duration(seconds: 1));

  // we're 600 pixels high, each item is 100 pixels high, scroll position is
  // 1000, so we should have exactly 6 items, 10..15.

  expect(find.text('0'), findsNothing, reason: 'with maintainState: $maintainState');
  expect(find.text('1'), findsNothing, reason: 'with maintainState: $maintainState');
  expect(find.text('8'), findsNothing, reason: 'with maintainState: $maintainState');
  expect(find.text('9'), findsNothing, reason: 'with maintainState: $maintainState');
  expect(find.text('10'), findsOneWidget, reason: 'with maintainState: $maintainState');
  expect(find.text('11'), findsOneWidget, reason: 'with maintainState: $maintainState');
  expect(find.text('12'), findsOneWidget, reason: 'with maintainState: $maintainState');
  expect(find.text('13'), findsOneWidget, reason: 'with maintainState: $maintainState');
  expect(find.text('14'), findsOneWidget, reason: 'with maintainState: $maintainState');
  expect(find.text('15'), findsOneWidget, reason: 'with maintainState: $maintainState');
  expect(find.text('16'), findsNothing, reason: 'with maintainState: $maintainState');
  expect(find.text('100'), findsNothing, reason: 'with maintainState: $maintainState');
}

void main() {
  testWidgets('ScrollPosition jumpTo() doesn\'t call notifyListeners twice', (WidgetTester tester) async {
    int count = 0;
    await tester.pumpWidget(MaterialApp(
      home: ListView.builder(
        itemBuilder: (BuildContext context, int index) {
          return Text('$index', textDirection: TextDirection.ltr);
        },
      ),
    ));

    final ScrollPosition position = tester.state<ScrollableState>(find.byType(Scrollable)).position;
    position.addListener(() {
      count++;
    });
    position.jumpTo(100);

    expect(count, 1);
  });

  testWidgets('whether we remember our scroll position', (WidgetTester tester) async {
    await performTest(tester, true);
    await performTest(tester, false);
  });

  testWidgets('scroll alignment is honored by ensureVisible', (WidgetTester tester) async {
    final List<int> items = List<int>.generate(11, (int index) => index).toList();
    final List<FocusNode> nodes = List<FocusNode>.generate(11, (int index) => FocusNode(debugLabel: 'Item ${index + 1}')).toList();
    final ScrollController controller = ScrollController();
    await tester.pumpWidget(
      MaterialApp(
        home: ListView(
          scrollDirection: Axis.vertical,
          controller: controller,
          children: items.map<Widget>((int item) {
            return Focus(
              key: ValueKey<int>(item),
              focusNode: nodes[item],
              child: Container(height: 110),
            );
          }).toList(),
        ),
      ),
    );

    controller.position.ensureVisible(
      tester.renderObject(find.byKey(const ValueKey<int>(0))),
      alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtStart,
    );
    expect(controller.position.pixels, equals(0.0));

    controller.position.ensureVisible(
      tester.renderObject(find.byKey(const ValueKey<int>(1))),
      alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtStart,
    );
    expect(controller.position.pixels, equals(0.0));

    controller.position.ensureVisible(
      tester.renderObject(find.byKey(const ValueKey<int>(1))),
      alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtEnd,
    );
    expect(controller.position.pixels, equals(0.0));

    controller.position.ensureVisible(
      tester.renderObject(find.byKey(const ValueKey<int>(4))),
      alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtStart,
    );
    expect(controller.position.pixels, equals(0.0));

    controller.position.ensureVisible(
      tester.renderObject(find.byKey(const ValueKey<int>(5))),
      alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtStart,
    );
    expect(controller.position.pixels, equals(0.0));

    controller.position.ensureVisible(
      tester.renderObject(find.byKey(const ValueKey<int>(5))),
      alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtEnd,
    );
    expect(controller.position.pixels, equals(60.0));

    controller.position.ensureVisible(
      tester.renderObject(find.byKey(const ValueKey<int>(0))),
      alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtStart,
    );
    expect(controller.position.pixels, equals(0.0));
  });

  testWidgets('jumpTo recomends deferred loading', (WidgetTester tester) async {
    int loadedWithDeferral = 0;
    int buildCount = 0;
    const double height = 500;
    await tester.pumpWidget(MaterialApp(
      home: ListView.builder(
        itemBuilder: (BuildContext context, int index) {
          buildCount += 1;
          if (Scrollable.recommendDeferredLoadingForContext(context)) {
            loadedWithDeferral += 1;
          }
          return const SizedBox(height: height);
        },
      ),
    ));

    // The two visible on screen should have loaded without deferral.
    expect(buildCount, 2);
    expect(loadedWithDeferral, 0);

    final ScrollPosition position = tester.state<ScrollableState>(find.byType(Scrollable)).position;
    position.jumpTo(height * 100);
    await tester.pump();

    // All but the first two that were loaded normally should have gotten a
    // recommendation to defer.
    expect(buildCount, 102);
    expect(loadedWithDeferral, 100);

    position.jumpTo(height * 102);
    await tester.pump();

    // The smaller jump should not have recommended deferral.
    expect(buildCount, 104);
    expect(loadedWithDeferral, 100);
  });
}
