// 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_test/flutter_test.dart';

class ExpandingBox extends StatefulWidget {
  const ExpandingBox({this.collapsedSize, this.expandedSize});

  final double collapsedSize;
  final double expandedSize;

  @override
  State<ExpandingBox> createState() => _ExpandingBoxState();
}

class _ExpandingBoxState extends State<ExpandingBox> with AutomaticKeepAliveClientMixin<ExpandingBox>{
  double _height;

  @override
  void initState() {
    super.initState();
    _height = widget.collapsedSize;
  }

  void toggleSize() {
    setState(() {
      _height = _height == widget.collapsedSize ? widget.expandedSize : widget.collapsedSize;
    });
  }

  @override
  Widget build(BuildContext context) {
    super.build(context);
    return Container(
      height: _height,
      color: Colors.green,
      child: Align(
        alignment: Alignment.bottomCenter,
        child: FlatButton(
          child: const Text('Collapse'),
          onPressed: toggleSize,
        ),
      ),
    );
  }

  @override
  bool get wantKeepAlive => true;
}

void main() {
  testWidgets('shrink listview', (WidgetTester tester) async {
    await tester.pumpWidget(MaterialApp(
      home: ListView.builder(
        itemBuilder: (BuildContext context, int index) => index == 0
              ? const ExpandingBox(collapsedSize: 400, expandedSize: 1200)
              : Container(height: 300, color: Colors.red),
        itemCount: 2,
      ),
    ));

    final ScrollPosition position = tester.state<ScrollableState>(find.byType(Scrollable)).position;
    expect(position.activity, isInstanceOf<IdleScrollActivity>());
    expect(position.minScrollExtent, 0.0);
    expect(position.maxScrollExtent, 100.0);
    expect(position.pixels, 0.0);
    await tester.tap(find.byType(FlatButton));
    await tester.pump();

    final TestGesture drag1 = await tester.startGesture(const Offset(10.0, 500.0));
    await tester.pump();
    await drag1.moveTo(const Offset(10.0, 0.0));
    await tester.pump();
    await drag1.up();
    await tester.pump();
    expect(position.pixels, moreOrLessEquals(500.0));
    expect(position.minScrollExtent, 0.0);
    expect(position.maxScrollExtent, 900.0);

    final TestGesture drag2 = await tester.startGesture(const Offset(10.0, 500.0));
    await tester.pump();
    await drag2.moveTo(const Offset(10.0, 100.0));
    await tester.pump();
    await drag2.up();
    await tester.pump();
    expect(position.maxScrollExtent, 900.0);
    expect(position.pixels, moreOrLessEquals(900.0));

    await tester.pump();
    await tester.tap(find.byType(FlatButton));
    await tester.pump();
    expect(position.minScrollExtent, 0.0);
    expect(position.maxScrollExtent, 100.0);
    expect(position.pixels, 100.0);
  });

  testWidgets('shrink listview while dragging', (WidgetTester tester) async {
    await tester.pumpWidget(MaterialApp(
      home: ListView.builder(
        itemBuilder: (BuildContext context, int index) => index == 0
              ? const ExpandingBox(collapsedSize: 400, expandedSize: 1200)
              : Container(height: 300, color: Colors.red),
        itemCount: 2,
      ),
    ));

    final ScrollPosition position = tester.state<ScrollableState>(find.byType(Scrollable)).position;
    expect(position.activity, isInstanceOf<IdleScrollActivity>());
    expect(position.minScrollExtent, 0.0);
    expect(position.maxScrollExtent, 100.0);
    expect(position.pixels, 0.0);
    await tester.tap(find.byType(FlatButton));
    await tester.pump(); // start button animation
    await tester.pump(const Duration(seconds: 1)); // finish button animation
    expect(position.minScrollExtent, 0.0);
    expect(position.maxScrollExtent, 1800.0);
    expect(position.pixels, 0.0);

    final TestGesture drag1 = await tester.startGesture(const Offset(10.0, 500.0));
    expect(await tester.pumpAndSettle(), 1); // Nothing to animate
    await drag1.moveTo(const Offset(10.0, 0.0));
    expect(await tester.pumpAndSettle(), 1); // Nothing to animate
    await drag1.up();
    expect(await tester.pumpAndSettle(), 1); // Nothing to animate
    expect(position.pixels, moreOrLessEquals(500.0));
    expect(position.minScrollExtent, 0.0);
    expect(position.maxScrollExtent, 900.0);

    final TestGesture drag2 = await tester.startGesture(const Offset(10.0, 500.0));
    expect(await tester.pumpAndSettle(), 1); // Nothing to animate
    await drag2.moveTo(const Offset(10.0, 100.0));
    expect(await tester.pumpAndSettle(), 1); // Nothing to animate
    expect(position.maxScrollExtent, 900.0);
    expect(position.pixels, lessThanOrEqualTo(900.0));
    expect(position.activity, isInstanceOf<DragScrollActivity>());

    final _ExpandingBoxState expandingBoxState = tester.state<_ExpandingBoxState>(find.byType(ExpandingBox));
    expandingBoxState.toggleSize();
    expect(await tester.pumpAndSettle(), 1); // Nothing to animate
    expect(position.activity, isInstanceOf<DragScrollActivity>());
    expect(position.minScrollExtent, 0.0);
    expect(position.maxScrollExtent, 100.0);
    expect(position.pixels, 100.0);

    await drag2.moveTo(const Offset(10.0, 150.0));
    await drag2.up();
    expect(position.minScrollExtent, 0.0);
    expect(position.maxScrollExtent, 100.0);
    expect(position.pixels, 50.0);
    expect(await tester.pumpAndSettle(), 1); // Nothing to animate
    expect(position.minScrollExtent, 0.0);
    expect(position.maxScrollExtent, 100.0);
    expect(position.pixels, 50.0);
  });

  testWidgets('shrink listview while ballistic', (WidgetTester tester) async {
    await tester.pumpWidget(MaterialApp(
      home: GestureDetector(
        onTap: () { assert(false); },
        child: ListView.builder(
          physics: const RangeMaintainingScrollPhysics(parent: BouncingScrollPhysics()),
          itemBuilder: (BuildContext context, int index) => index == 0
                ? const ExpandingBox(collapsedSize: 400, expandedSize: 1200)
                : Container(height: 300, color: Colors.red),
          itemCount: 2,
        ),
      ),
    ));

    final _ExpandingBoxState expandingBoxState = tester.state<_ExpandingBoxState>(find.byType(ExpandingBox));
    expandingBoxState.toggleSize();

    final ScrollPosition position = tester.state<ScrollableState>(find.byType(Scrollable)).position;
    expect(position.activity, isInstanceOf<IdleScrollActivity>());
    expect(position.minScrollExtent, 0.0);
    expect(position.maxScrollExtent, 100.0);
    expect(position.pixels, 0.0);
    await tester.pump();
    expect(position.minScrollExtent, 0.0);
    expect(position.maxScrollExtent, 1800.0);
    expect(position.pixels, 0.0);

    final TestGesture drag1 = await tester.startGesture(const Offset(10.0, 10.0));
    await tester.pump();
    expect(position.activity, isInstanceOf<HoldScrollActivity>());
    expect(position.minScrollExtent, 0.0);
    expect(position.maxScrollExtent, 1800.0);
    expect(position.pixels, 0.0);
    await drag1.moveTo(const Offset(10.0, 50.0)); // to get past the slop and trigger the drag
    await drag1.moveTo(const Offset(10.0, 550.0));
    expect(position.pixels, -500.0);
    await tester.pump();
    expect(position.activity, isInstanceOf<DragScrollActivity>());
    expect(position.minScrollExtent, 0.0);
    expect(position.maxScrollExtent, 1800.0);
    expect(position.pixels, -500.0);
    await drag1.up();
    await tester.pump();
    expect(position.activity, isInstanceOf<BallisticScrollActivity>());
    expect(position.minScrollExtent, 0.0);
    expect(position.maxScrollExtent, 1800.0);
    expect(position.pixels, -500.0);

    expandingBoxState.toggleSize();
    await tester.pump(); // apply physics without moving clock forward
    expect(position.activity, isInstanceOf<BallisticScrollActivity>());
    // TODO(ianh): Determine why the maxScrollOffset is 200.0 here instead of 100.0 or double.infinity.
    // expect(position.minScrollExtent, 0.0);
    // expect(position.maxScrollExtent, 100.0);
    expect(position.pixels, -500.0);

    await tester.pumpAndSettle(); // ignoring the exact effects of the animation
    expect(position.activity, isInstanceOf<IdleScrollActivity>());
    expect(position.minScrollExtent, 0.0);
    expect(position.maxScrollExtent, 100.0);
    expect(position.pixels, 0.0);
  });
}