// 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 'dart:ui' as ui;

import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
  final LayerLink link = LayerLink();

  testWidgets('Change link during layout', (WidgetTester tester) async {
    final GlobalKey key = GlobalKey();
    Widget build({ LayerLink? linkToUse }) {
      return Directionality(
        textDirection: TextDirection.ltr,
        // The LayoutBuilder forces the CompositedTransformTarget widget to
        // access its own size when [RenderObject.debugActiveLayout] is
        // non-null.
        child: LayoutBuilder(
          builder: (BuildContext context, BoxConstraints constraints) {
            return Stack(
            children: <Widget>[
              Positioned(
                left: 123.0,
                top: 456.0,
                child: CompositedTransformTarget(
                  link: linkToUse ?? link,
                  child: const SizedBox(height: 10.0, width: 10.0),
                ),
              ),
              Positioned(
                left: 787.0,
                top: 343.0,
                child: CompositedTransformFollower(
                  link: linkToUse ?? link,
                  targetAnchor: Alignment.center,
                  followerAnchor: Alignment.center,
                  child: SizedBox(key: key, height: 20.0, width: 20.0),
                ),
              ),
            ],
          );
          },
        ),
      );
    }

    await tester.pumpWidget(build());
    final RenderBox box = key.currentContext!.findRenderObject()! as RenderBox;
    expect(box.localToGlobal(Offset.zero), const Offset(118.0, 451.0));

    await tester.pumpWidget(build(linkToUse: LayerLink()));
    expect(box.localToGlobal(Offset.zero), const Offset(118.0, 451.0));
  });

  group('Composited transforms - only offsets', () {
    final GlobalKey key = GlobalKey();

    Widget build({ required Alignment targetAlignment, required Alignment followerAlignment }) {
      return Directionality(
        textDirection: TextDirection.ltr,
        child: Stack(
          children: <Widget>[
            Positioned(
              left: 123.0,
              top: 456.0,
              child: CompositedTransformTarget(
                link: link,
                child: const SizedBox(height: 10.0, width: 10.0),
              ),
            ),
            Positioned(
              left: 787.0,
              top: 343.0,
              child: CompositedTransformFollower(
                link: link,
                targetAnchor: targetAlignment,
                followerAnchor: followerAlignment,
                child: SizedBox(key: key, height: 20.0, width: 20.0),
              ),
            ),
          ],
        ),
      );
    }

    testWidgets('topLeft', (WidgetTester tester) async {
      await tester.pumpWidget(build(targetAlignment: Alignment.topLeft, followerAlignment: Alignment.topLeft));
      final RenderBox box = key.currentContext!.findRenderObject()! as RenderBox;
      expect(box.localToGlobal(Offset.zero), const Offset(123.0, 456.0));
    });

    testWidgets('center', (WidgetTester tester) async {
      await tester.pumpWidget(build(targetAlignment: Alignment.center, followerAlignment: Alignment.center));
      final RenderBox box = key.currentContext!.findRenderObject()! as RenderBox;
      expect(box.localToGlobal(Offset.zero), const Offset(118.0, 451.0));
    });

    testWidgets('bottomRight - topRight', (WidgetTester tester) async {
      await tester.pumpWidget(build(targetAlignment: Alignment.bottomRight, followerAlignment: Alignment.topRight));
      final RenderBox box = key.currentContext!.findRenderObject()! as RenderBox;
      expect(box.localToGlobal(Offset.zero), const Offset(113.0, 466.0));
    });
  });

  group('Composited transforms - with rotations', () {
    final GlobalKey key1 = GlobalKey();
    final GlobalKey key2 = GlobalKey();

    Widget build({ required Alignment targetAlignment, required Alignment followerAlignment }) {
      return Directionality(
        textDirection: TextDirection.ltr,
        child: Stack(
          children: <Widget>[
            Positioned(
              top: 123.0,
              left: 456.0,
              child: Transform.rotate(
                angle: 1.0, // radians
                child: CompositedTransformTarget(
                  link: link,
                  child: SizedBox(key: key1, width: 80.0, height: 10.0),
                ),
              ),
            ),
            Positioned(
              top: 787.0,
              left: 343.0,
              child: Transform.rotate(
                angle: -0.3, // radians
                child: CompositedTransformFollower(
                  link: link,
                  targetAnchor: targetAlignment,
                  followerAnchor: followerAlignment,
                  child: SizedBox(key: key2, width: 40.0, height: 20.0),
                ),
              ),
            ),
          ],
        ),
      );
    }
    testWidgets('topLeft', (WidgetTester tester) async {
      await tester.pumpWidget(build(targetAlignment: Alignment.topLeft, followerAlignment: Alignment.topLeft));
      final RenderBox box1 = key1.currentContext!.findRenderObject()! as RenderBox;
      final RenderBox box2 = key2.currentContext!.findRenderObject()! as RenderBox;
      final Offset position1 = box1.localToGlobal(Offset.zero);
      final Offset position2 = box2.localToGlobal(Offset.zero);
      expect(position1, offsetMoreOrLessEquals(position2));
    });

    testWidgets('center', (WidgetTester tester) async {
      await tester.pumpWidget(build(targetAlignment: Alignment.center, followerAlignment: Alignment.center));
      final RenderBox box1 = key1.currentContext!.findRenderObject()! as RenderBox;
      final RenderBox box2 = key2.currentContext!.findRenderObject()! as RenderBox;
      final Offset position1 = box1.localToGlobal(const Offset(40, 5));
      final Offset position2 = box2.localToGlobal(const Offset(20, 10));
      expect(position1, offsetMoreOrLessEquals(position2));
    });

    testWidgets('bottomRight - topRight', (WidgetTester tester) async {
      await tester.pumpWidget(build(targetAlignment: Alignment.bottomRight, followerAlignment: Alignment.topRight));
      final RenderBox box1 = key1.currentContext!.findRenderObject()! as RenderBox;
      final RenderBox box2 = key2.currentContext!.findRenderObject()! as RenderBox;
      final Offset position1 = box1.localToGlobal(const Offset(80, 10));
      final Offset position2 = box2.localToGlobal(const Offset(40, 0));
      expect(position1, offsetMoreOrLessEquals(position2));
    });
  });

  group('Composited transforms - nested', () {
    final GlobalKey key1 = GlobalKey();
    final GlobalKey key2 = GlobalKey();

    Widget build({ required Alignment targetAlignment, required Alignment followerAlignment }) {
      return Directionality(
        textDirection: TextDirection.ltr,
        child: Stack(
          children: <Widget>[
            Positioned(
              top: 123.0,
              left: 456.0,
              child: Transform.rotate(
                angle: 1.0, // radians
                child: CompositedTransformTarget(
                  link: link,
                  child: SizedBox(key: key1, width: 80.0, height: 10.0),
                ),
              ),
            ),
            Positioned(
              top: 787.0,
              left: 343.0,
              child: Transform.rotate(
                angle: -0.3, // radians
                child: Padding(
                  padding: const EdgeInsets.all(20.0),
                  child: CompositedTransformFollower(
                    link: LayerLink(),
                    child: Transform(
                      transform: Matrix4.skew(0.9, 1.1),
                      child: Padding(
                        padding: const EdgeInsets.all(20.0),
                        child: CompositedTransformFollower(
                          link: link,
                          targetAnchor: targetAlignment,
                          followerAnchor: followerAlignment,
                          child: SizedBox(key: key2, width: 40.0, height: 20.0),
                        ),
                      ),
                    ),
                  ),
                ),
              ),
            ),
          ],
        ),
      );
    }
    testWidgets('topLeft', (WidgetTester tester) async {
      await tester.pumpWidget(build(targetAlignment: Alignment.topLeft, followerAlignment: Alignment.topLeft));
      final RenderBox box1 = key1.currentContext!.findRenderObject()! as RenderBox;
      final RenderBox box2 = key2.currentContext!.findRenderObject()! as RenderBox;
      final Offset position1 = box1.localToGlobal(Offset.zero);
      final Offset position2 = box2.localToGlobal(Offset.zero);
      expect(position1, offsetMoreOrLessEquals(position2));
    });

    testWidgets('center', (WidgetTester tester) async {
      await tester.pumpWidget(build(targetAlignment: Alignment.center, followerAlignment: Alignment.center));
      final RenderBox box1 = key1.currentContext!.findRenderObject()! as RenderBox;
      final RenderBox box2 = key2.currentContext!.findRenderObject()! as RenderBox;
      final Offset position1 = box1.localToGlobal(Alignment.center.alongSize(const Size(80, 10)));
      final Offset position2 = box2.localToGlobal(Alignment.center.alongSize(const Size(40, 20)));
      expect(position1, offsetMoreOrLessEquals(position2));
    });

    testWidgets('bottomRight - topRight', (WidgetTester tester) async {
      await tester.pumpWidget(build(targetAlignment: Alignment.bottomRight, followerAlignment: Alignment.topRight));
      final RenderBox box1 = key1.currentContext!.findRenderObject()! as RenderBox;
      final RenderBox box2 = key2.currentContext!.findRenderObject()! as RenderBox;
      final Offset position1 = box1.localToGlobal(Alignment.bottomRight.alongSize(const Size(80, 10)));
      final Offset position2 = box2.localToGlobal(Alignment.topRight.alongSize(const Size(40, 20)));
      expect(position1, offsetMoreOrLessEquals(position2));
    });
  });

  group('Composited transforms - hit testing', () {
    final GlobalKey key1 = GlobalKey();
    final GlobalKey key2 = GlobalKey();
    final GlobalKey key3 = GlobalKey();

    bool tapped = false;

    Widget build({ required Alignment targetAlignment, required Alignment followerAlignment }) {
      return Directionality(
        textDirection: TextDirection.ltr,
        child: Stack(
          children: <Widget>[
            Positioned(
              left: 123.0,
              top: 456.0,
              child: CompositedTransformTarget(
                link: link,
                child: SizedBox(key: key1, height: 10.0, width: 10.0),
              ),
            ),
            CompositedTransformFollower(
              link: link,
              child: GestureDetector(
                key: key2,
                behavior: HitTestBehavior.opaque,
                onTap: () { tapped = true; },
                child: SizedBox(key: key3, height: 2.0, width: 2.0),
              ),
            ),
          ],
        ),
      );
    }

    const List<Alignment> alignments = <Alignment>[
      Alignment.topLeft, Alignment.topRight,
      Alignment.center,
      Alignment.bottomLeft, Alignment.bottomRight,
    ];

    setUp(() { tapped = false; });

    for (final Alignment targetAlignment in alignments) {
      for (final Alignment followerAlignment in alignments) {
        testWidgets('$targetAlignment - $followerAlignment', (WidgetTester tester) async{
          await tester.pumpWidget(build(targetAlignment: targetAlignment, followerAlignment: followerAlignment));
          final RenderBox box2 = key2.currentContext!.findRenderObject()! as RenderBox;
          expect(box2.size, const Size(2.0, 2.0));
          expect(tapped, isFalse);
          await tester.tap(find.byKey(key3), warnIfMissed: false); // the container itself is transparent to hits
          expect(tapped, isTrue);
        });
      }
    }
  });

  testWidgets('Leader after Follower asserts', (WidgetTester tester) async {
    final LayerLink link = LayerLink();
    await tester.pumpWidget(
      CompositedTransformFollower(
        link: link,
        child: CompositedTransformTarget(
          link: link,
          child: const SizedBox(height: 20, width: 20),
        ),
      ),
    );

    expect(
      (tester.takeException() as AssertionError).message,
      contains('LeaderLayer anchor must come before FollowerLayer in paint order'),
    );
  });

  testWidgets(
      '`FollowerLayer` (`CompositedTransformFollower`) has null pointer error when using with some kinds of `Layer`s',
      (WidgetTester tester) async {
    final LayerLink link = LayerLink();
    await tester.pumpWidget(
      CompositedTransformTarget(
        link: link,
        child: CompositedTransformFollower(
          link: link,
          child: const _CustomWidget(),
        ),
      ),
    );
  });
}

class _CustomWidget extends SingleChildRenderObjectWidget {
  const _CustomWidget();

  @override
  _CustomRenderObject createRenderObject(BuildContext context) => _CustomRenderObject();

  @override
  void updateRenderObject(BuildContext context, _CustomRenderObject renderObject) {}
}

class _CustomRenderObject extends RenderProxyBox {
  _CustomRenderObject({RenderBox? child}) : super(child);

  @override
  void paint(PaintingContext context, Offset offset) {
    if (layer == null) {
      layer = _CustomLayer(
        computeSomething: _computeSomething,
      );
    } else {
      (layer as _CustomLayer?)?.computeSomething = _computeSomething;
    }

    context.pushLayer(layer!, super.paint, Offset.zero);
  }

  void _computeSomething() {
    // indeed, use `globalToLocal` to compute some useful data
    globalToLocal(Offset.zero);
  }
}

class _CustomLayer extends ContainerLayer {
  _CustomLayer({required this.computeSomething});

  VoidCallback computeSomething;

  @override
  void addToScene(ui.SceneBuilder builder) {
    computeSomething(); // indeed, need to use result of this function
    super.addToScene(builder);
  }
}