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

@Tags(<String>['reduced-test-set'])
library;

import 'package:fake_async/fake_async.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart';

class _MockAnimationController extends AnimationController {
  _MockAnimationController()
      : super(duration: const Duration(minutes: 1), vsync: const TestVSync());
  int forwardCalls = 0;
  int reverseCalls = 0;

  @override
  TickerFuture forward({double? from}) {
    forwardCalls++;
    return super.forward(from: from);
  }

  @override
  TickerFuture reverse({double? from}) {
    reverseCalls++;
    return super.reverse(from: from);
  }
}

void main() {
  Future<T> runFakeAsync<T>(Future<T> Function(FakeAsync time) f) async {
    return FakeAsync().run((FakeAsync time) async {
      bool pump = true;
      final Future<T> future = f(time).whenComplete(() => pump = false);
      while (pump) {
        time.flushMicrotasks();
      }
      return future;
    });
  }

  group('Raw Magnifier', () {
    testWidgetsWithLeakTracking('should render with correct focal point and decoration',
        (WidgetTester tester) async {
      final Key appKey = UniqueKey();
      const Size magnifierSize = Size(100, 100);
      const Offset magnifierFocalPoint = Offset(50, 50);
      const Offset magnifierPosition = Offset(200, 200);
      const double magnificationScale = 2;

      await tester.pumpWidget(MaterialApp(
          key: appKey,
          home: Container(
            color: Colors.orange,
            width: double.infinity,
            height: double.infinity,
            child: Stack(
              children: <Widget>[
                Positioned(
                  // Positioned so that it is right in the center of the magnifier
                  // focal point.
                  left: magnifierPosition.dx + magnifierFocalPoint.dx,
                  top: magnifierPosition.dy + magnifierFocalPoint.dy,
                  child: Container(
                    color: Colors.pink,
                    // Since it is the size of the magnifier but over its
                    // magnificationScale, it should take up the whole magnifier.
                    width: (magnifierSize.width * 1.5) / magnificationScale,
                    height: (magnifierSize.height * 1.5) / magnificationScale,
                  ),
                ),
                Positioned(
                  left: magnifierPosition.dx,
                  top: magnifierPosition.dy,
                  child: const RawMagnifier(
                    size: magnifierSize,
                    focalPointOffset: magnifierFocalPoint,
                    magnificationScale: magnificationScale,
                    decoration: MagnifierDecoration(shadows: <BoxShadow>[
                      BoxShadow(
                        spreadRadius: 10,
                        blurRadius: 10,
                        color: Colors.green,
                        offset: Offset(5, 5),
                      ),
                    ]),
                  ),
                ),
              ],
            ),
          )));

      await tester.pumpAndSettle();

      // Should look like an orange screen, with two pink boxes.
      // One pink box is in the magnifier (so has a green shadow) and is double
      // size (from magnification). Also, the magnifier should be slightly orange
      // since it has opacity.
      await expectLater(
        find.byKey(appKey),
        matchesGoldenFile('widgets.magnifier.styled.png'),
      );
    }, skip: kIsWeb);  // [intended] Bdf does not display on web.

    group('transition states', () {
      final AnimationController animationController = AnimationController(
          vsync: const TestVSync(), duration: const Duration(minutes: 2));
      final MagnifierController magnifierController = MagnifierController();

      tearDown(() {
        animationController.value = 0;
        magnifierController.hide();

        magnifierController.removeFromOverlay();
      });

      testWidgetsWithLeakTracking(
          'should immediately remove from overlay on no animation controller',
          (WidgetTester tester) async {
        await runFakeAsync((FakeAsync async) async {
          const RawMagnifier testMagnifier = RawMagnifier(
            size: Size(100, 100),
          );

          await tester.pumpWidget(const MaterialApp(
            home: Placeholder(),
          ));

          final BuildContext context =
              tester.firstElement(find.byType(Placeholder));

          magnifierController.show(
            context: context,
            builder: (BuildContext context) => testMagnifier,
          );

          WidgetsBinding.instance.scheduleFrame();
          await tester.pump();

          expect(magnifierController.overlayEntry, isNot(isNull));

          magnifierController.hide();
          WidgetsBinding.instance.scheduleFrame();
          await tester.pump();

          expect(magnifierController.overlayEntry, isNull);
        });
      });

      testWidgetsWithLeakTracking('should update shown based on animation status',
          (WidgetTester tester) async {
        await runFakeAsync((FakeAsync async) async {
          final MagnifierController magnifierController =
              MagnifierController(animationController: animationController);

          const RawMagnifier testMagnifier = RawMagnifier(
            size: Size(100, 100),
          );

          await tester.pumpWidget(const MaterialApp(
            home: Placeholder(),
          ));

          final BuildContext context =
              tester.firstElement(find.byType(Placeholder));

          magnifierController.show(
            context: context,
            builder: (BuildContext context) => testMagnifier,
          );

          WidgetsBinding.instance.scheduleFrame();
          await tester.pump();

          // No time has passed, so the animation controller has not completed.
          expect(magnifierController.animationController?.status,
              AnimationStatus.forward);
          expect(magnifierController.shown, true);

          async.elapse(animationController.duration!);
          await tester.pumpAndSettle();

          expect(magnifierController.animationController?.status,
              AnimationStatus.completed);
          expect(magnifierController.shown, true);

          magnifierController.hide();

          WidgetsBinding.instance.scheduleFrame();
          await tester.pump();

          expect(magnifierController.animationController?.status,
              AnimationStatus.reverse);
          expect(magnifierController.shown, false);

          async.elapse(animationController.duration!);
          await tester.pumpAndSettle();

          expect(magnifierController.animationController?.status,
              AnimationStatus.dismissed);
          expect(magnifierController.shown, false);
        });
      });
    });
  });

  group('magnifier controller', () {
    final MagnifierController magnifierController = MagnifierController();

    tearDown(() {
      magnifierController.removeFromOverlay();
    });

    group('show', () {
      testWidgetsWithLeakTracking('should insert below below widget', (WidgetTester tester) async {
        await tester.pumpWidget(const MaterialApp(
          home: Text('text'),
        ));

        final BuildContext context = tester.firstElement(find.byType(Text));

        final Widget fakeMagnifier = Placeholder(key: UniqueKey());
        final Widget fakeBefore = Placeholder(key: UniqueKey());

        final OverlayEntry fakeBeforeOverlayEntry =
            OverlayEntry(builder: (_) => fakeBefore);
        addTearDown(() => fakeBeforeOverlayEntry..remove()..dispose());

        Overlay.of(context).insert(fakeBeforeOverlayEntry);
        magnifierController.show(
            context: context,
            builder: (_) => fakeMagnifier,
            below: fakeBeforeOverlayEntry);

        WidgetsBinding.instance.scheduleFrame();
        await tester.pumpAndSettle();

        final Iterable<Element> allOverlayChildren = find
            .descendant(
                of: find.byType(Overlay), matching: find.byType(Placeholder))
            .evaluate();

        // Expect the magnifier to be the first child, even though it was inserted
        // after the fakeBefore.
        expect(allOverlayChildren.last.widget.key, fakeBefore.key);
        expect(allOverlayChildren.first.widget.key, fakeMagnifier.key);
      });

      testWidgetsWithLeakTracking('should insert newly built widget without animating out if overlay != null',
          (WidgetTester tester) async {
        await runFakeAsync((FakeAsync async) async {
          final _MockAnimationController animationController =
              _MockAnimationController();

          const RawMagnifier testMagnifier = RawMagnifier(
            size: Size(100, 100),
          );
          const RawMagnifier testMagnifier2 = RawMagnifier(
            size: Size(100, 100),
          );

          await tester.pumpWidget(const MaterialApp(
            home: Placeholder(),
          ));

          final BuildContext context =
              tester.firstElement(find.byType(Placeholder));

          magnifierController.show(
            context: context,
            builder: (BuildContext context) => testMagnifier,
          );

          WidgetsBinding.instance.scheduleFrame();
          await tester.pump();

          async.elapse(animationController.duration!);
          await tester.pumpAndSettle();

          magnifierController.show(context: context, builder: (_) => testMagnifier2);

          WidgetsBinding.instance.scheduleFrame();
          await tester.pump();

          expect(animationController.reverseCalls, 0,
              reason:
                  'should not have called reverse on animation controller due to force remove');

          expect(find.byWidget(testMagnifier2), findsOneWidget);
        });
      });
    });

    group('shift within bounds', () {
      final List<Rect> boundsRects = <Rect>[
        const Rect.fromLTRB(0, 0, 100, 100),
        const Rect.fromLTRB(0, 0, 100, 100),
        const Rect.fromLTRB(0, 0, 100, 100),
        const Rect.fromLTRB(0, 0, 100, 100),
      ];
      final List<Rect> inputRects = <Rect>[
        const Rect.fromLTRB(-100, -100, -80, -80),
        const Rect.fromLTRB(0, 0, 20, 20),
        const Rect.fromLTRB(110, 0, 120, 10),
        const Rect.fromLTRB(110, 110, 120, 120)
      ];
      final List<Rect> outputRects = <Rect>[
        const Rect.fromLTRB(0, 0, 20, 20),
        const Rect.fromLTRB(0, 0, 20, 20),
        const Rect.fromLTRB(90, 0, 100, 10),
        const Rect.fromLTRB(90, 90, 100, 100)
      ];

      for (int i = 0; i < boundsRects.length; i++) {
        test(
            'should shift ${inputRects[i]} to ${outputRects[i]} for bounds ${boundsRects[i]}',
            () {
          final Rect outputRect = MagnifierController.shiftWithinBounds(
              bounds: boundsRects[i], rect: inputRects[i]);
          expect(outputRect, outputRects[i]);
        });
      }
    });
  });
}