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

class TestSliverPersistentHeaderDelegate extends SliverPersistentHeaderDelegate {
  TestSliverPersistentHeaderDelegate(this._maxExtent);

  final double _maxExtent;

  @override
  double get maxExtent => _maxExtent;

  @override
  double get minExtent => 16.0;

  @override
  Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
    return Column(
      children: <Widget>[
        Container(height: minExtent),
        Expanded(child: Container()),
      ],
    );
  }

  @override
  bool shouldRebuild(TestSliverPersistentHeaderDelegate oldDelegate) => false;
}

class TestBehavior extends ScrollBehavior {
  const TestBehavior();

  @override
  Widget buildOverscrollIndicator(BuildContext context, Widget child, ScrollableDetails details) {
    return GlowingOverscrollIndicator(
      axisDirection: details.direction,
      color: const Color(0xFFFFFFFF),
      child: child,
    );
  }
}

class TestScrollPhysics extends ClampingScrollPhysics {
  const TestScrollPhysics({ ScrollPhysics? parent }) : super(parent: parent);

  @override
  TestScrollPhysics applyTo(ScrollPhysics? ancestor) {
    return TestScrollPhysics(parent: parent?.applyTo(ancestor) ?? ancestor);
  }

  @override
  Tolerance get tolerance => const Tolerance(velocity: 20.0, distance: 1.0);
}

class TestViewportScrollPosition extends ScrollPositionWithSingleContext {
  TestViewportScrollPosition({
    required ScrollPhysics physics,
    required ScrollContext context,
    ScrollPosition? oldPosition,
  }) : super(physics: physics, context: context, oldPosition: oldPosition);

  @override
  bool applyContentDimensions(double minScrollExtent, double maxScrollExtent) {
    expect(minScrollExtent, moreOrLessEquals(-3895.0));
    expect(maxScrollExtent, moreOrLessEquals(8575.0));
    return super.applyContentDimensions(minScrollExtent, maxScrollExtent);
  }
}

void main() {
  testWidgets('Evil test of sliver features - 1', (WidgetTester tester) async {
    final GlobalKey centerKey = GlobalKey();
    await tester.pumpWidget(
      MediaQuery(
        data: const MediaQueryData(),
        child: Directionality(
          textDirection: TextDirection.ltr,
          child: ScrollConfiguration(
            behavior: const TestBehavior(),
            child: Scrollbar(
              child: Scrollable(
                axisDirection: AxisDirection.down,
                physics: const TestScrollPhysics(),
                viewportBuilder: (BuildContext context, ViewportOffset offset) {
                  return Viewport(
                    axisDirection: AxisDirection.down,
                    anchor: 0.25,
                    offset: offset,
                    center: centerKey,
                    slivers: <Widget>[
                      SliverToBoxAdapter(child: Container(height: 5.0)),
                      SliverToBoxAdapter(child: Container(height: 520.0)),
                      SliverToBoxAdapter(child: Container(height: 520.0)),
                      SliverToBoxAdapter(child: Container(height: 520.0)),
                      SliverPersistentHeader(delegate: TestSliverPersistentHeaderDelegate(150.0), pinned: true),
                      SliverToBoxAdapter(child: Container(height: 520.0)),
                      SliverPadding(
                        padding: const EdgeInsets.all(50.0),
                        sliver: SliverToBoxAdapter(child: Container(height: 520.0)),
                      ),
                      SliverToBoxAdapter(child: Container(height: 520.0)),
                      SliverPersistentHeader(delegate: TestSliverPersistentHeaderDelegate(150.0), floating: true),
                      SliverToBoxAdapter(child: Container(height: 520.0)),
                      SliverToBoxAdapter(key: centerKey, child: Container(height: 520.0)), // ------------------------ CENTER ------------------------
                      SliverPersistentHeader(delegate: TestSliverPersistentHeaderDelegate(150.0), pinned: true),
                      SliverToBoxAdapter(child: Container(height: 520.0)),
                      SliverToBoxAdapter(child: Container(height: 520.0)),
                      SliverToBoxAdapter(child: Container(height: 520.0)),
                      SliverPadding(
                        padding: const EdgeInsets.all(50.0),
                        sliver: SliverPersistentHeader(delegate: TestSliverPersistentHeaderDelegate(250.0), pinned: true),
                      ),
                      SliverToBoxAdapter(child: Container(height: 520.0)),
                      SliverPersistentHeader(delegate: TestSliverPersistentHeaderDelegate(250.0), pinned: true),
                      SliverToBoxAdapter(child: Container(height: 5.0)),
                      SliverPersistentHeader(delegate: TestSliverPersistentHeaderDelegate(250.0), pinned: true),
                      SliverToBoxAdapter(child: Container(height: 5.0)),
                      SliverPersistentHeader(delegate: TestSliverPersistentHeaderDelegate(250.0), pinned: true),
                      SliverPersistentHeader(delegate: TestSliverPersistentHeaderDelegate(250.0), pinned: true),
                      SliverToBoxAdapter(child: Container(height: 5.0)),
                      SliverPersistentHeader(delegate: TestSliverPersistentHeaderDelegate(250.0), pinned: true),
                      SliverPersistentHeader(delegate: TestSliverPersistentHeaderDelegate(250.0)),
                      SliverToBoxAdapter(child: Container(height: 520.0)),
                      SliverPersistentHeader(delegate: TestSliverPersistentHeaderDelegate(150.0), floating: true),
                      SliverToBoxAdapter(child: Container(height: 520.0)),
                      SliverPersistentHeader(delegate: TestSliverPersistentHeaderDelegate(150.0), floating: true),
                      SliverToBoxAdapter(child: Container(height: 5.0)),
                      SliverList(
                        delegate: SliverChildListDelegate(<Widget>[
                          Container(height: 50.0),
                          Container(height: 50.0),
                          Container(height: 50.0),
                          Container(height: 50.0),
                          Container(height: 50.0),
                          Container(height: 50.0),
                          Container(height: 50.0),
                          Container(height: 50.0),
                          Container(height: 50.0),
                          Container(height: 50.0),
                          Container(height: 50.0),
                          Container(height: 50.0),
                          Container(height: 50.0),
                          Container(height: 50.0),
                          Container(height: 50.0),
                        ]),
                      ),
                      SliverPersistentHeader(delegate: TestSliverPersistentHeaderDelegate(250.0)),
                      SliverPersistentHeader(delegate: TestSliverPersistentHeaderDelegate(250.0)),
                      SliverPersistentHeader(delegate: TestSliverPersistentHeaderDelegate(250.0)),
                      SliverPadding(
                        padding: const EdgeInsets.symmetric(horizontal: 50.0),
                        sliver: SliverToBoxAdapter(child: Container(height: 520.0)),
                      ),
                      SliverToBoxAdapter(child: Container(height: 520.0)),
                      SliverToBoxAdapter(child: Container(height: 520.0)),
                      SliverToBoxAdapter(child: Container(height: 5.0)),
                    ],
                  );
                },
              ),
            ),
          ),
        ),
      ),
    );
    final ScrollPosition position = tester.state<ScrollableState>(find.byType(Scrollable)).position;

    position.animateTo(10000.0, curve: Curves.linear, duration: const Duration(minutes: 1));
    await tester.pump(const Duration(milliseconds: 10));
    await tester.pump(const Duration(milliseconds: 10));
    await tester.pump(const Duration(milliseconds: 50));
    await tester.pumpAndSettle(const Duration(milliseconds: 122));

    position.animateTo(-10000.0, curve: Curves.linear, duration: const Duration(minutes: 1));
    await tester.pump(const Duration(milliseconds: 10));
    await tester.pump(const Duration(milliseconds: 10));
    await tester.pump(const Duration(milliseconds: 50));
    await tester.pumpAndSettle(const Duration(milliseconds: 122));

    position.animateTo(10000.0, curve: Curves.linear, duration: const Duration(minutes: 1));
    await tester.pump(const Duration(milliseconds: 10));
    await tester.pump(const Duration(milliseconds: 10));
    await tester.pump(const Duration(milliseconds: 50));
    await tester.pumpAndSettle(const Duration(milliseconds: 122));

    position.animateTo(-10000.0, curve: Curves.linear, duration: const Duration(seconds: 1));
    await tester.pump(const Duration(milliseconds: 10));
    await tester.pump(const Duration(milliseconds: 10));
    await tester.pump(const Duration(milliseconds: 50));
    await tester.pumpAndSettle(const Duration(milliseconds: 122));

    position.animateTo(10000.0, curve: Curves.linear, duration: const Duration(seconds: 1));
    await tester.pump(const Duration(milliseconds: 10));
    await tester.pump(const Duration(milliseconds: 10));
    await tester.pump(const Duration(milliseconds: 50));
    await tester.pumpAndSettle(const Duration(milliseconds: 122));

  });

  testWidgets('Removing offscreen items above and rescrolling does not crash', (WidgetTester tester) async {
    await tester.pumpWidget(MaterialApp(
      home: CustomScrollView(
        cacheExtent: 0.0,
        slivers: <Widget>[
          SliverFixedExtentList(
            itemExtent: 100.0,
            delegate: SliverChildBuilderDelegate(
              (BuildContext context, int index) {
                return Container(
                  color: Colors.blue,
                  child: Text(index.toString()),
                );
              },
              childCount: 30,
            ),
          ),
        ],
      ),
    ));

    await tester.drag(find.text('5'), const Offset(0.0, -500.0));
    await tester.pump();

    // Screen is 600px high. Moved bottom item 500px up. It's now at the top.
    expect(tester.getTopLeft(find.widgetWithText(Container, '5')).dy, 0.0);
    expect(tester.getBottomLeft(find.widgetWithText(Container, '10')).dy, 600.0);

    // Stop returning the first 3 items.
    await tester.pumpWidget(MaterialApp(
      home: CustomScrollView(
        cacheExtent: 0.0,
        slivers: <Widget>[
          SliverFixedExtentList(
            itemExtent: 100.0,
            delegate: SliverChildBuilderDelegate(
              (BuildContext context, int index) {
                if (index > 3) {
                  return Container(
                    color: Colors.blue,
                    child: Text(index.toString()),
                  );
                }
                return null;
              },
              childCount: 30,
            ),
          ),
        ],
      ),
    ));

    await tester.drag(find.text('5'), const Offset(0.0, 400.0));
    await tester.pump();

    // Move up by 4 items, meaning item 1 would have been at the top but
    // 0 through 3 no longer exist, so item 4, 3 items down, is the first one.
    // Item 4 is also shifted to the top.
    expect(tester.getTopLeft(find.widgetWithText(Container, '4')).dy, 0.0);

    // Because the screen is still 600px, item 9 is now visible at the bottom instead
    // of what's supposed to be item 6 had we not re-shifted.
    expect(tester.getBottomLeft(find.widgetWithText(Container, '9')).dy, 600.0);
  });
}