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

void verifyPaintPosition(GlobalKey key, Offset ideal) {
  final RenderObject target = key.currentContext!.findRenderObject()!;
  expect(target.parent, isA<RenderViewport>());
  final SliverPhysicalParentData parentData = target.parentData! as SliverPhysicalParentData;
  final Offset actual = parentData.paintOffset;
  expect(actual, ideal);
}

void main() {
  testWidgets('Sliver appbars - scrolling', (WidgetTester tester) async {
    GlobalKey key1, key2, key3, key4, key5;
    await tester.pumpWidget(
      Directionality(
        textDirection: TextDirection.ltr,
        child: CustomScrollView(
          slivers: <Widget>[
            BigSliver(key: key1 = GlobalKey()),
            SliverPersistentHeader(key: key2 = GlobalKey(), delegate: TestDelegate()),
            SliverPersistentHeader(key: key3 = GlobalKey(), delegate: TestDelegate()),
            BigSliver(key: key4 = GlobalKey()),
            BigSliver(key: key5 = GlobalKey()),
          ],
        ),
      ),
    );
    final ScrollPosition position = tester.state<ScrollableState>(find.byType(Scrollable)).position;
    final double max =
        RenderBigSliver.height * 3.0 +
        TestDelegate().maxExtent * 2.0 -
        600.0; // 600 is the height of the test viewport
    assert(max < 10000.0);
    expect(max, 1450.0);
    expect(position.pixels, 0.0);
    expect(position.minScrollExtent, 0.0);
    expect(position.maxScrollExtent, max);
    position.animateTo(10000.0, curve: Curves.linear, duration: const Duration(minutes: 1));
    await tester.pumpAndSettle(const Duration(milliseconds: 10));
    expect(position.pixels, max);
    expect(position.minScrollExtent, 0.0);
    expect(position.maxScrollExtent, max);
    verifyPaintPosition(key1, Offset.zero);
    verifyPaintPosition(key2, Offset.zero);
    verifyPaintPosition(key3, Offset.zero);
    verifyPaintPosition(key4, Offset.zero);
    verifyPaintPosition(key5, const Offset(0.0, 50.0));
  });

  testWidgets('Sliver appbars - scrolling off screen', (WidgetTester tester) async {
    final GlobalKey key = GlobalKey();
    final TestDelegate delegate = TestDelegate();
    await tester.pumpWidget(
      Directionality(
        textDirection: TextDirection.ltr,
        child: CustomScrollView(
          slivers: <Widget>[
            const BigSliver(),
            SliverPersistentHeader(key: key, delegate: delegate),
            const BigSliver(),
            const BigSliver(),
          ],
        ),
      ),
    );
    final ScrollPosition position = tester.state<ScrollableState>(find.byType(Scrollable)).position;
    position.animateTo(
      RenderBigSliver.height + delegate.maxExtent - 5.0,
      curve: Curves.linear,
      duration: const Duration(minutes: 1),
    );
    await tester.pumpAndSettle(const Duration(milliseconds: 1000));
    final RenderBox box = tester.renderObject<RenderBox>(find.text('Sliver App Bar'));
    final Rect rect = Rect.fromPoints(
      box.localToGlobal(Offset.zero),
      box.localToGlobal(box.size.bottomRight(Offset.zero)),
    );
    expect(rect, equals(const Rect.fromLTWH(0.0, -195.0, 800.0, 200.0)));
  });

  testWidgets('Sliver appbars - scrolling - overscroll gap is below header', (
    WidgetTester tester,
  ) async {
    await tester.pumpWidget(
      Directionality(
        textDirection: TextDirection.ltr,
        child: CustomScrollView(
          physics: const BouncingScrollPhysics(),
          slivers: <Widget>[
            SliverPersistentHeader(delegate: TestDelegate()),
            SliverList(
              delegate: SliverChildListDelegate(<Widget>[
                const SizedBox(height: 300.0, child: Text('X')),
              ]),
            ),
          ],
        ),
      ),
    );

    expect(tester.getTopLeft(find.text('Sliver App Bar')), Offset.zero);
    expect(tester.getTopLeft(find.text('X')), const Offset(0.0, 200.0));

    final ScrollPosition position = tester.state<ScrollableState>(find.byType(Scrollable)).position;
    position.jumpTo(-50.0);
    await tester.pump();

    expect(tester.getTopLeft(find.text('Sliver App Bar')), Offset.zero);
    expect(tester.getTopLeft(find.text('X')), const Offset(0.0, 250.0));
  });

  testWidgets('Sliver appbars const child delegate - scrolling - overscroll gap is below header', (
    WidgetTester tester,
  ) async {
    await tester.pumpWidget(
      Directionality(
        textDirection: TextDirection.ltr,
        child: CustomScrollView(
          physics: const BouncingScrollPhysics(),
          slivers: <Widget>[
            SliverPersistentHeader(delegate: TestDelegate()),
            const SliverList(
              delegate: SliverChildListDelegate.fixed(<Widget>[
                SizedBox(height: 300.0, child: Text('X')),
              ]),
            ),
          ],
        ),
      ),
    );

    expect(tester.getTopLeft(find.text('Sliver App Bar')), Offset.zero);
    expect(tester.getTopLeft(find.text('X')), const Offset(0.0, 200.0));

    final ScrollPosition position = tester.state<ScrollableState>(find.byType(Scrollable)).position;
    position.jumpTo(-50.0);
    await tester.pump();

    expect(tester.getTopLeft(find.text('Sliver App Bar')), Offset.zero);
    expect(tester.getTopLeft(find.text('X')), const Offset(0.0, 250.0));
  });

  group('has correct semantics when', () {
    testWidgets('within viewport', (WidgetTester tester) async {
      const double cacheExtent = 250;
      final SemanticsHandle handle = tester.ensureSemantics();

      await tester.pumpWidget(
        Directionality(
          textDirection: TextDirection.ltr,
          child: CustomScrollView(
            cacheExtent: cacheExtent,
            physics: const BouncingScrollPhysics(),
            slivers: <Widget>[
              SliverPersistentHeader(delegate: TestDelegate()),
              const SliverList(
                delegate: SliverChildListDelegate.fixed(<Widget>[
                  SizedBox(height: 300.0, child: Text('X')),
                ]),
              ),
            ],
          ),
        ),
      );

      final SemanticsFinder sliverAppBar = find.semantics.byLabel('Sliver App Bar');

      expect(sliverAppBar, findsOne);
      expect(sliverAppBar, containsSemantics(isHidden: false));
      handle.dispose();
    });

    testWidgets('partially scrolling off screen', (WidgetTester tester) async {
      final GlobalKey key = GlobalKey();
      final TestDelegate delegate = TestDelegate();
      final SemanticsHandle handle = tester.ensureSemantics();
      const double cacheExtent = 250;
      await tester.pumpWidget(
        Directionality(
          textDirection: TextDirection.ltr,
          child: CustomScrollView(
            cacheExtent: cacheExtent,
            slivers: <Widget>[
              SliverPersistentHeader(key: key, delegate: delegate),
              const BigSliver(),
              const BigSliver(),
            ],
          ),
        ),
      );
      final ScrollPosition position = tester
          .state<ScrollableState>(find.byType(Scrollable))
          .position;
      position.animateTo(
        delegate.maxExtent - 20.0,
        curve: Curves.linear,
        duration: const Duration(minutes: 1),
      );
      await tester.pumpAndSettle(const Duration(milliseconds: 1000));
      final RenderBox box = tester.renderObject<RenderBox>(find.text('Sliver App Bar'));
      final Rect rect = Rect.fromPoints(
        box.localToGlobal(Offset.zero),
        box.localToGlobal(box.size.bottomRight(Offset.zero)),
      );
      expect(rect, equals(const Rect.fromLTWH(0.0, -180.0, 800.0, 200.0)));

      final SemanticsFinder sliverAppBar = find.semantics.byLabel('Sliver App Bar');

      expect(sliverAppBar, findsOne);
      expect(sliverAppBar, containsSemantics(isHidden: false));
      handle.dispose();
    });

    testWidgets('completely scrolling off screen but within cache extent', (
      WidgetTester tester,
    ) async {
      final GlobalKey key = GlobalKey();
      final TestDelegate delegate = TestDelegate();
      final SemanticsHandle handle = tester.ensureSemantics();
      const double cacheExtent = 250;
      await tester.pumpWidget(
        Directionality(
          textDirection: TextDirection.ltr,
          child: CustomScrollView(
            cacheExtent: cacheExtent,
            slivers: <Widget>[
              SliverPersistentHeader(key: key, delegate: delegate),
              const BigSliver(),
              const BigSliver(),
            ],
          ),
        ),
      );
      final ScrollPosition position = tester
          .state<ScrollableState>(find.byType(Scrollable))
          .position;
      position.animateTo(
        delegate.maxExtent + 20.0,
        curve: Curves.linear,
        duration: const Duration(minutes: 1),
      );
      await tester.pumpAndSettle(const Duration(milliseconds: 1000));

      final SemanticsFinder sliverAppBar = find.semantics.byLabel('Sliver App Bar');

      expect(sliverAppBar, findsOne);
      expect(sliverAppBar, containsSemantics(isHidden: true));
      handle.dispose();
    });

    testWidgets('completely scrolling off screen and not within cache extent', (
      WidgetTester tester,
    ) async {
      final GlobalKey key = GlobalKey();
      final TestDelegate delegate = TestDelegate();
      final SemanticsHandle handle = tester.ensureSemantics();
      const double cacheExtent = 250;
      await tester.pumpWidget(
        Directionality(
          textDirection: TextDirection.ltr,
          child: CustomScrollView(
            cacheExtent: cacheExtent,
            slivers: <Widget>[
              SliverPersistentHeader(key: key, delegate: delegate),
              const BigSliver(),
              const BigSliver(),
            ],
          ),
        ),
      );
      final ScrollPosition position = tester
          .state<ScrollableState>(find.byType(Scrollable))
          .position;
      position.animateTo(
        delegate.maxExtent + 300.0,
        curve: Curves.linear,
        duration: const Duration(minutes: 1),
      );
      await tester.pumpAndSettle(const Duration(milliseconds: 1000));

      final SemanticsFinder sliverAppBar = find.semantics.byLabel('Sliver App Bar');

      expect(sliverAppBar, findsNothing);
      handle.dispose();
    });
  });
}

class TestDelegate extends SliverPersistentHeaderDelegate {
  @override
  double get maxExtent => 200.0;

  @override
  double get minExtent => 200.0;

  @override
  Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
    return SizedBox(height: maxExtent, child: const Text('Sliver App Bar'));
  }

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

class RenderBigSliver extends RenderSliver {
  static const double height = 550.0;
  double get paintExtent =>
      (height - constraints.scrollOffset).clamp(0.0, constraints.remainingPaintExtent);

  @override
  void performLayout() {
    geometry = SliverGeometry(
      scrollExtent: height,
      paintExtent: paintExtent,
      maxPaintExtent: height,
    );
  }
}

class BigSliver extends LeafRenderObjectWidget {
  const BigSliver({super.key});
  @override
  RenderBigSliver createRenderObject(BuildContext context) {
    return RenderBigSliver();
  }
}
