// 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';

import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:vector_math/vector_math_64.dart';

void main() {
  test('ContainerLayer.findAllAnnotations returns all results from its children', () {
    final Layer root = _Layers(
      ContainerLayer(),
      children: <Object>[
        _TestAnnotatedLayer(1, opaque: false),
        _TestAnnotatedLayer(2, opaque: false),
        _TestAnnotatedLayer(3, opaque: false),
      ],
    ).build();

    expect(
      root.findAllAnnotations<int>(Offset.zero).entries.toList(),
      _equalToAnnotationResult<int>(<AnnotationEntry<int>>[
        const AnnotationEntry<int>(annotation: 3, localPosition: Offset.zero),
        const AnnotationEntry<int>(annotation: 2, localPosition: Offset.zero),
        const AnnotationEntry<int>(annotation: 1, localPosition: Offset.zero),
      ]),
    );
  });

  test('ContainerLayer.find returns the first result from its children', () {
    final Layer root = _Layers(
      ContainerLayer(),
      children: <Object>[
        _TestAnnotatedLayer(1, opaque: false),
        _TestAnnotatedLayer(2, opaque: false),
        _TestAnnotatedLayer(3, opaque: false),
      ],
    ).build();

    final int result = root.find<int>(Offset.zero)!;
    expect(result, 3);
  });

  test('ContainerLayer.findAllAnnotations returns empty result when finding nothing', () {
    final Layer root = _Layers(
      ContainerLayer(),
      children: <Object>[
        _TestAnnotatedLayer(1, opaque: false),
        _TestAnnotatedLayer(2, opaque: false),
        _TestAnnotatedLayer(3, opaque: false),
      ],
    ).build();

    expect(root.findAllAnnotations<double>(Offset.zero).entries.isEmpty, isTrue);
  });

  test('ContainerLayer.find returns null when finding nothing', () {
    final Layer root = _Layers(
      ContainerLayer(),
      children: <Object>[
        _TestAnnotatedLayer(1, opaque: false),
        _TestAnnotatedLayer(2, opaque: false),
        _TestAnnotatedLayer(3, opaque: false),
      ],
    ).build();

    expect(root.find<double>(Offset.zero), isNull);
  });

  test('ContainerLayer.findAllAnnotations stops at the first opaque child', () {
    final Layer root = _Layers(
      ContainerLayer(),
      children: <Object>[
        _TestAnnotatedLayer(1, opaque: false),
        _TestAnnotatedLayer(2, opaque: true),
        _TestAnnotatedLayer(3, opaque: false),
      ],
    ).build();

    expect(
      root.findAllAnnotations<int>(Offset.zero).entries.toList(),
      _equalToAnnotationResult<int>(<AnnotationEntry<int>>[
        const AnnotationEntry<int>(annotation: 3, localPosition: Offset.zero),
        const AnnotationEntry<int>(annotation: 2, localPosition: Offset.zero),
      ]),
    );
  });

  test("ContainerLayer.findAllAnnotations returns children's opacity (true)", () {
    final Layer root = _withBackgroundAnnotation(
      1000,
      _Layers(
        ContainerLayer(),
        children: <Object>[
          _TestAnnotatedLayer(2, opaque: true),
        ],
      ).build(),
    );

    expect(
      root.findAllAnnotations<int>(Offset.zero).entries.toList(),
      _equalToAnnotationResult<int>(<AnnotationEntry<int>>[
        const AnnotationEntry<int>(annotation: 2, localPosition: Offset.zero),
      ]),
    );
  });

  test("ContainerLayer.findAllAnnotations returns children's opacity (false)", () {
    final Layer root = _withBackgroundAnnotation(
      1000,
      _Layers(
        ContainerLayer(),
        children: <Object>[
          _TestAnnotatedLayer(2, opaque: false),
        ],
      ).build(),
    );

    expect(
      root.findAllAnnotations<int>(Offset.zero).entries.toList(),
      _equalToAnnotationResult<int>(<AnnotationEntry<int>>[
        const AnnotationEntry<int>(annotation: 2, localPosition: Offset.zero),
        const AnnotationEntry<int>(annotation: 1000, localPosition: Offset.zero),
      ]),
    );
  });

  test('ContainerLayer.findAllAnnotations returns false as opacity when finding nothing', () {
    final Layer root = _withBackgroundAnnotation(
      1000,
      _Layers(
        ContainerLayer(),
        children: <Object>[
          _TestAnnotatedLayer(2, opaque: false, size: Size.zero),
        ],
      ).build(),
    );

    expect(
      root.findAllAnnotations<int>(Offset.zero).entries.toList(),
      _equalToAnnotationResult<int>(<AnnotationEntry<int>>[
        const AnnotationEntry<int>(annotation: 1000, localPosition: Offset.zero),
      ]),
    );
  });

  test('OffsetLayer.findAllAnnotations respects offset', () {
    const Offset insidePosition = Offset(-5, 5);
    const Offset outsidePosition = Offset(5, 5);

    final Layer root = _withBackgroundAnnotation(
      1000,
      _Layers(
        OffsetLayer(offset: const Offset(-10, 0)),
        children: <Object>[
          _TestAnnotatedLayer(1, opaque: true, size: const Size(10, 10)),
        ],
      ).build(),
    );

    expect(
      root.findAllAnnotations<int>(insidePosition).entries.toList(),
      _equalToAnnotationResult<int>(<AnnotationEntry<int>>[
        const AnnotationEntry<int>(annotation: 1, localPosition: Offset(5, 5)),
      ]),
    );
    expect(
      root.findAllAnnotations<int>(outsidePosition).entries.toList(),
      _equalToAnnotationResult<int>(<AnnotationEntry<int>>[
        const AnnotationEntry<int>(annotation: 1000, localPosition: Offset(5, 5)),
      ]),
    );
  });

  test('ClipRectLayer.findAllAnnotations respects clipRect', () {
    const Offset insidePosition = Offset(11, 11);
    const Offset outsidePosition = Offset(19, 19);

    final Layer root = _withBackgroundAnnotation(
      1000,
      _Layers(
        ClipRectLayer(clipRect: const Offset(10, 10) & const Size(5, 5)),
        children: <Object>[
          _TestAnnotatedLayer(
            1,
            opaque: true,
            size: const Size(10, 10),
            offset: const Offset(10, 10),
          ),
        ],
      ).build(),
    );

    expect(
      root.findAllAnnotations<int>(insidePosition).entries.toList(),
      _equalToAnnotationResult<int>(<AnnotationEntry<int>>[
        const AnnotationEntry<int>(annotation: 1, localPosition: insidePosition),
      ]),
    );
    expect(
      root.findAllAnnotations<int>(outsidePosition).entries.toList(),
      _equalToAnnotationResult<int>(<AnnotationEntry<int>>[
        const AnnotationEntry<int>(annotation: 1000, localPosition: outsidePosition),
      ]),
    );
  });

  test('ClipRRectLayer.findAllAnnotations respects clipRRect', () {
    // For a curve of radius 4 centered at (4, 4),
    // location (1, 1) is outside, while (2, 2) is inside.
    // Here we shift this RRect by (10, 10).
    final RRect rrect = RRect.fromRectAndRadius(
      const Offset(10, 10) & const Size(10, 10),
      const Radius.circular(4),
    );
    const Offset insidePosition = Offset(12, 12);
    const Offset outsidePosition = Offset(11, 11);

    final Layer root = _withBackgroundAnnotation(
      1000,
      _Layers(
        ClipRRectLayer(clipRRect: rrect),
        children: <Object>[
          _TestAnnotatedLayer(
            1,
            opaque: true,
            size: const Size(10, 10),
            offset: const Offset(10, 10),
          ),
        ],
      ).build(),
    );

    expect(
      root.findAllAnnotations<int>(insidePosition).entries.toList(),
      _equalToAnnotationResult<int>(<AnnotationEntry<int>>[
        const AnnotationEntry<int>(annotation: 1, localPosition: insidePosition),
      ]),
    );
    expect(
      root.findAllAnnotations<int>(outsidePosition).entries.toList(),
      _equalToAnnotationResult<int>(<AnnotationEntry<int>>[
        const AnnotationEntry<int>(annotation: 1000, localPosition: outsidePosition),
      ]),
    );
  });

  test('ClipPathLayer.findAllAnnotations respects clipPath', () {
    // For this triangle, location (1, 1) is inside, while (2, 2) is outside.
    //         2
    //    —————
    //    |  /
    //    | /
    // 2  |/
    final Path originalPath = Path();
    originalPath.lineTo(2, 0);
    originalPath.lineTo(0, 2);
    originalPath.close();
    // Shift this clip path by (10, 10).
    final Path path = originalPath.shift(const Offset(10, 10));
    const Offset insidePosition = Offset(11, 11);
    const Offset outsidePosition = Offset(12, 12);

    final Layer root = _withBackgroundAnnotation(
      1000,
      _Layers(
        ClipPathLayer(clipPath: path),
        children: <Object>[
          _TestAnnotatedLayer(
            1,
            opaque: true,
            size: const Size(10, 10),
            offset: const Offset(10, 10),
          ),
        ],
      ).build(),
    );

    expect(
      root.findAllAnnotations<int>(insidePosition).entries.toList(),
      _equalToAnnotationResult<int>(<AnnotationEntry<int>>[
        const AnnotationEntry<int>(annotation: 1, localPosition: insidePosition),
      ]),
    );
    expect(
      root.findAllAnnotations<int>(outsidePosition).entries.toList(),
      _equalToAnnotationResult<int>(<AnnotationEntry<int>>[
        const AnnotationEntry<int>(annotation: 1000, localPosition: outsidePosition),
      ]),
    );
  });

  test('TransformLayer.findAllAnnotations respects transform', () {
    // Matrix `transform` enlarges the target by (2x, 4x), then shift it by
    // (10, 20).
    final Matrix4 transform = Matrix4.diagonal3Values(2, 4, 1)..setTranslation(Vector3(10, 20, 0));
    // The original region is Offset(10, 10) & Size(10, 10)
    // The transformed region is Offset(30, 60) & Size(20, 40)
    const Offset insidePosition = Offset(40, 80);
    const Offset outsidePosition = Offset(20, 40);

    final Layer root = _withBackgroundAnnotation(
      1000,
      _Layers(
        TransformLayer(transform: transform),
        children: <Object>[
          _TestAnnotatedLayer(
            1,
            opaque: true,
            size: const Size(10, 10),
            offset: const Offset(10, 10),
          ),
        ],
      ).build(),
    );

    expect(
      root.findAllAnnotations<int>(insidePosition).entries.toList(),
      _equalToAnnotationResult<int>(<AnnotationEntry<int>>[
        const AnnotationEntry<int>(annotation: 1, localPosition: Offset(15, 15)),
      ]),
    );
    expect(
      root.findAllAnnotations<int>(outsidePosition).entries.toList(),
      _equalToAnnotationResult<int>(<AnnotationEntry<int>>[
        const AnnotationEntry<int>(annotation: 1000, localPosition: outsidePosition),
      ]),
    );
  });

  test('TransformLayer.findAllAnnotations correctly transforms with perspective', () {
    // Test the 4 corners of a transformed annotated region.
    final Matrix4 transform = Matrix4.identity()
      ..setEntry(3, 2, 0.005)
      ..rotateX(-0.2)
      ..rotateY(0.2);

    final Layer root = _withBackgroundAnnotation(
      0,
      _Layers(
        TransformLayer(transform: transform),
        children: <Object>[
          _TestAnnotatedLayer(
            1,
            opaque: true,
            size: const Size(30, 40),
            offset: const Offset(10, 20),
          ),
        ],
      ).build(),
    );

    void expectOneAnnotation({
      required Offset globalPosition,
      required int value,
      required Offset localPosition,
    }) {
      expect(
        root.findAllAnnotations<int>(globalPosition).entries.toList(),
        _equalToAnnotationResult<int>(
          <AnnotationEntry<int>>[
            AnnotationEntry<int>(annotation: value, localPosition: localPosition),
          ],
          maxCoordinateRelativeDiff: 0.005,
        ),
      );
    }

    expectOneAnnotation(
      globalPosition: const Offset(10.0, 19.7),
      value: 0,
      localPosition: const Offset(10.0, 19.7),
    );
    expectOneAnnotation(
      globalPosition: const Offset(10.1, 19.8),
      value: 1,
      localPosition: const Offset(10.0, 20.0),
    );

    expectOneAnnotation(
      globalPosition: const Offset(10.5, 62.8),
      value: 0,
      localPosition: const Offset(10.5, 62.8),
    );
    expectOneAnnotation(
      globalPosition: const Offset(10.6, 62.7),
      value: 1,
      localPosition: const Offset(10.1, 59.9),
    );

    expectOneAnnotation(
      globalPosition: const Offset(42.6, 40.8),
      value: 0,
      localPosition: const Offset(42.6, 40.8),
    );
    expectOneAnnotation(
      globalPosition: const Offset(42.5, 40.9),
      value: 1,
      localPosition: const Offset(39.9, 40.0),
    );

    expectOneAnnotation(
      globalPosition: const Offset(43.5, 63.5),
      value: 0,
      localPosition: const Offset(43.5, 63.5),
    );
    expectOneAnnotation(
      globalPosition: const Offset(43.4, 63.4),
      value: 1,
      localPosition: const Offset(39.9, 59.9),
    );
  });

  test('TransformLayer.findAllAnnotations skips when transform is irreversible', () {
    final Matrix4 transform = Matrix4.diagonal3Values(1, 0, 1);

    final Layer root = _withBackgroundAnnotation(
      1000,
      _Layers(
        TransformLayer(transform: transform),
        children: <Object>[
          _TestAnnotatedLayer(1, opaque: true),
        ],
      ).build(),
    );

    expect(
      root.findAllAnnotations<int>(Offset.zero).entries.toList(),
      _equalToAnnotationResult<int>(<AnnotationEntry<int>>[
        const AnnotationEntry<int>(annotation: 1000, localPosition: Offset.zero),
      ]),
    );
  });

  test('PhysicalModelLayer.findAllAnnotations respects clipPath', () {
    // For this triangle, location (1, 1) is inside, while (2, 2) is outside.
    //         2
    //    —————
    //    |  /
    //    | /
    // 2  |/
    final Path originalPath = Path();
    originalPath.lineTo(2, 0);
    originalPath.lineTo(0, 2);
    originalPath.close();
    // Shift this clip path by (10, 10).
    final Path path = originalPath.shift(const Offset(10, 10));
    const Offset insidePosition = Offset(11, 11);
    const Offset outsidePosition = Offset(12, 12);

    final Layer root = _withBackgroundAnnotation(
      1000,
      _Layers(
        PhysicalModelLayer(
          clipPath: path,
          elevation: 10,
          color: const Color.fromARGB(0, 0, 0, 0),
          shadowColor: const Color.fromARGB(0, 0, 0, 0),
        ),
        children: <Object>[
          _TestAnnotatedLayer(
            1,
            opaque: true,
            size: const Size(10, 10),
            offset: const Offset(10, 10),
          ),
        ],
      ).build(),
    );

    expect(
      root.findAllAnnotations<int>(insidePosition).entries.toList(),
      _equalToAnnotationResult<int>(<AnnotationEntry<int>>[
        const AnnotationEntry<int>(annotation: 1, localPosition: insidePosition),
      ]),
    );
    expect(
      root.findAllAnnotations<int>(outsidePosition).entries.toList(),
      _equalToAnnotationResult<int>(<AnnotationEntry<int>>[
        const AnnotationEntry<int>(annotation: 1000, localPosition: outsidePosition),
      ]),
    );
  });

  test('LeaderLayer.findAllAnnotations respects offset', () {
    const Offset insidePosition = Offset(-5, 5);
    const Offset outsidePosition = Offset(5, 5);

    final Layer root = _withBackgroundAnnotation(
      1000,
      _Layers(
        LeaderLayer(
          link: LayerLink(),
          offset: const Offset(-10, 0),
        ),
        children: <Object>[
          _TestAnnotatedLayer(1, opaque: true, size: const Size(10, 10)),
        ],
      ).build(),
    );

    expect(
      root.findAllAnnotations<int>(insidePosition).entries.toList(),
      _equalToAnnotationResult<int>(<AnnotationEntry<int>>[
        const AnnotationEntry<int>(annotation: 1, localPosition: Offset(5, 5)),
      ]),
    );
    expect(
      root.findAllAnnotations<int>(outsidePosition).entries.toList(),
      _equalToAnnotationResult<int>(<AnnotationEntry<int>>[
        const AnnotationEntry<int>(annotation: 1000, localPosition: outsidePosition),
      ]),
    );
  });

  test(
    'AnnotatedRegionLayer.findAllAnnotations should append to the list '
    'and return the given opacity (false) during a successful hit',
    () {
      const Offset position = Offset(5, 5);

      final Layer root = _withBackgroundAnnotation(
        1000,
        _Layers(
          AnnotatedRegionLayer<int>(1, opaque: false),
          children: <Object>[
            _TestAnnotatedLayer(2, opaque: false),
          ],
        ).build(),
      );

      expect(
        root.findAllAnnotations<int>(position).entries.toList(),
        _equalToAnnotationResult<int>(<AnnotationEntry<int>>[
          const AnnotationEntry<int>(annotation: 2, localPosition: position),
          const AnnotationEntry<int>(annotation: 1, localPosition: position),
          const AnnotationEntry<int>(annotation: 1000, localPosition: position),
        ]),
      );
    },
  );

  test(
    'AnnotatedRegionLayer.findAllAnnotations should append to the list '
    'and return the given opacity (true) during a successful hit',
    () {
      const Offset position = Offset(5, 5);

      final Layer root = _withBackgroundAnnotation(
        1000,
        _Layers(
          AnnotatedRegionLayer<int>(1, opaque: true),
          children: <Object>[
            _TestAnnotatedLayer(2, opaque: false),
          ],
        ).build(),
      );

      expect(
        root.findAllAnnotations<int>(position).entries.toList(),
        _equalToAnnotationResult<int>(<AnnotationEntry<int>>[
          const AnnotationEntry<int>(annotation: 2, localPosition: position),
          const AnnotationEntry<int>(annotation: 1, localPosition: position),
        ]),
      );
    },
  );

  test('AnnotatedRegionLayer.findAllAnnotations has default opacity as false', () {
    const Offset position = Offset(5, 5);

    final Layer root = _withBackgroundAnnotation(
      1000,
      _Layers(
        AnnotatedRegionLayer<int>(1),
        children: <Object>[
          _TestAnnotatedLayer(2, opaque: false),
        ],
      ).build(),
    );

    expect(
      root.findAllAnnotations<int>(position).entries.toList(),
      _equalToAnnotationResult<int>(<AnnotationEntry<int>>[
        const AnnotationEntry<int>(annotation: 2, localPosition: position),
        const AnnotationEntry<int>(annotation: 1, localPosition: position),
        const AnnotationEntry<int>(annotation: 1000, localPosition: position),
      ]),
    );
  });

  test(
    'AnnotatedRegionLayer.findAllAnnotations should still check children and return '
    "children's opacity (false) during a failed hit",
    () {
      const Offset position = Offset(5, 5);

      final Layer root = _withBackgroundAnnotation(
        1000,
        _Layers(
          AnnotatedRegionLayer<int>(1, opaque: true, size: Size.zero),
          children: <Object>[
            _TestAnnotatedLayer(2, opaque: false),
          ],
        ).build(),
      );

      expect(
        root.findAllAnnotations<int>(position).entries.toList(),
        _equalToAnnotationResult<int>(<AnnotationEntry<int>>[
          const AnnotationEntry<int>(annotation: 2, localPosition: position),
          const AnnotationEntry<int>(annotation: 1000, localPosition: position),
        ]),
      );
    },
  );

  test(
    'AnnotatedRegionLayer.findAllAnnotations should still check children and return '
    "children's opacity (true) during a failed hit",
    () {
      const Offset position = Offset(5, 5);

      final Layer root = _withBackgroundAnnotation(
        1000,
        _Layers(
          AnnotatedRegionLayer<int>(1, opaque: false, size: Size.zero),
          children: <Object>[
            _TestAnnotatedLayer(2, opaque: true),
          ],
        ).build(),
      );

      expect(
        root.findAllAnnotations<int>(position).entries.toList(),
        _equalToAnnotationResult<int>(<AnnotationEntry<int>>[
          const AnnotationEntry<int>(annotation: 2, localPosition: position),
        ]),
      );
    },
  );

  test(
    "AnnotatedRegionLayer.findAllAnnotations should not add to children's opacity "
    'during a successful hit if it is not opaque',
    () {
      const Offset position = Offset(5, 5);

      final Layer root = _withBackgroundAnnotation(
        1000,
        _Layers(
          AnnotatedRegionLayer<int>(1, opaque: false),
          children: <Object>[
            _TestAnnotatedLayer(2, opaque: false),
          ],
        ).build(),
      );

      expect(
        root.findAllAnnotations<int>(position).entries.toList(),
        _equalToAnnotationResult<int>(<AnnotationEntry<int>>[
          const AnnotationEntry<int>(annotation: 2, localPosition: position),
          const AnnotationEntry<int>(annotation: 1, localPosition: position),
          const AnnotationEntry<int>(annotation: 1000, localPosition: position),
        ]),
      );
    },
  );

  test(
    "AnnotatedRegionLayer.findAllAnnotations should add to children's opacity "
    'during a successful hit if it is opaque',
    () {
      const Offset position = Offset(5, 5);

      final Layer root = _withBackgroundAnnotation(
        1000,
        _Layers(
          AnnotatedRegionLayer<int>(1, opaque: true),
          children: <Object>[
            _TestAnnotatedLayer(2, opaque: false),
          ],
        ).build(),
      );

      expect(
        root.findAllAnnotations<int>(position).entries.toList(),
        _equalToAnnotationResult<int>(<AnnotationEntry<int>>[
          const AnnotationEntry<int>(annotation: 2, localPosition: position),
          const AnnotationEntry<int>(annotation: 1, localPosition: position),
        ]),
      );
    },
  );

  test(
    'AnnotatedRegionLayer.findAllAnnotations should clip its annotation '
    'using size and offset (positive)',
    () {
      // The target position would have fallen outside if not for the offset.
      const Offset position = Offset(100, 100);

      final Layer root = _withBackgroundAnnotation(
        1000,
        _Layers(
          AnnotatedRegionLayer<int>(
            1,
            size: const Size(20, 20),
            offset: const Offset(90, 90),
          ),
          children: <Object>[
            _TestAnnotatedLayer(
              2,
              opaque: false,
              // Use this offset to make sure AnnotatedRegionLayer's offset
              // does not affect its children.
              offset: const Offset(20, 20),
              size: const Size(110, 110),
            ),
          ],
        ).build(),
      );

      expect(
        root.findAllAnnotations<int>(position).entries.toList(),
        _equalToAnnotationResult<int>(<AnnotationEntry<int>>[
          const AnnotationEntry<int>(annotation: 2, localPosition: position),
          const AnnotationEntry<int>(annotation: 1, localPosition: Offset(10, 10)),
          const AnnotationEntry<int>(annotation: 1000, localPosition: position),
        ]),
      );
    },
  );

  test(
    'AnnotatedRegionLayer.findAllAnnotations should clip its annotation '
    'using size and offset (negative)',
    () {
      // The target position would have fallen inside if not for the offset.
      const Offset position = Offset(10, 10);

      final Layer root = _withBackgroundAnnotation(
        1000,
        _Layers(
          AnnotatedRegionLayer<int>(
            1,
            size: const Size(20, 20),
            offset: const Offset(90, 90),
          ),
          children: <Object>[
            _TestAnnotatedLayer(2, opaque: false, size: const Size(110, 110)),
          ],
        ).build(),
      );

      expect(
        root.findAllAnnotations<int>(position).entries.toList(),
        _equalToAnnotationResult<int>(<AnnotationEntry<int>>[
          const AnnotationEntry<int>(annotation: 2, localPosition: position),
          const AnnotationEntry<int>(annotation: 1000, localPosition: position),
        ]),
      );
    },
  );
}

/// A [ContainerLayer] that contains a stack of layers: `layer` in the front,
/// and another layer annotated with `value` in the back.
///
/// It is a utility function that helps checking the opacity returned by
/// [Layer.findAnnotations].
Layer _withBackgroundAnnotation(int value, Layer layer) {
  return _Layers(
    ContainerLayer(),
    children: <Object>[
      _TestAnnotatedLayer(value, opaque: false),
      layer,
    ],
  ).build();
}

// A utility class that helps building a layer tree.
class _Layers {
  _Layers(this.root, {this.children});

  final ContainerLayer root;
  // Each element must be instance of Layer or _Layers.
  final List<Object>? children;
  bool _assigned = false;

  // Build the layer tree by calling each child's `build`, then append children
  // to [root]. Returns the root.
  Layer build() {
    assert(!_assigned);
    _assigned = true;
    if (children != null) {
      for (final Object child in children!) {
        late Layer layer;
        if (child is Layer) {
          layer = child;
        } else if (child is _Layers) {
          layer = child.build();
        } else {
          assert(false, 'Element of _Layers.children must be instance of Layer or _Layers');
        }
        root.append(layer);
      }
    }
    return root;
  }
}

// This layer's [findAnnotation] can be controlled by the given arguments.
class _TestAnnotatedLayer extends Layer {
  _TestAnnotatedLayer(
    this.value, {
    required this.opaque,
    this.offset = Offset.zero,
    this.size,
  });

  // The value added to result in [findAnnotations] during a successful hit.
  final int value;

  // The return value of [findAnnotations] during a successful hit.
  final bool opaque;

  /// The [offset] is optionally used to translate the clip region for the
  /// hit-testing of [find] by [offset].
  ///
  /// If not provided, offset defaults to [Offset.zero].
  ///
  /// Ignored if [size] is not set.
  final Offset offset;

  /// The [size] is optionally used to clip the hit-testing of [find].
  ///
  /// If not provided, all offsets are considered to be contained within this
  /// layer, unless an ancestor layer applies a clip.
  ///
  /// If [offset] is set, then the offset is applied to the size region before
  /// hit testing in [find].
  final Size? size;

  @override
  EngineLayer? addToScene(SceneBuilder builder, [Offset layerOffset = Offset.zero]) {
    return null;
  }

  // This implementation is hit when the type is `int` and position is within
  // [offset] & [size]. If it is hit, it adds [value] to result and returns
  // [opaque]; otherwise it directly returns false.
  @override
  bool findAnnotations<S extends Object>(
    AnnotationResult<S> result,
    Offset localPosition, {
    required bool onlyFirst,
  }) {
    if (S != int)
      return false;
    if (size != null && !(offset & size!).contains(localPosition))
      return false;
    final Object untypedValue = value;
    final S typedValue = untypedValue as S;
    result.add(AnnotationEntry<S>(annotation: typedValue, localPosition: localPosition));
    return opaque;
  }
}

bool _almostEqual(double a, double b, double maxRelativeDiff) {
  assert(maxRelativeDiff >= 0);
  assert(maxRelativeDiff < 1);
  return (a - b).abs() <= a.abs() * maxRelativeDiff;
}

Matcher _equalToAnnotationResult<T>(
  List<AnnotationEntry<int>> list, {
  double maxCoordinateRelativeDiff = 0,
}) {
  return pairwiseCompare<AnnotationEntry<int>, AnnotationEntry<int>>(
    list,
    (AnnotationEntry<int> a, AnnotationEntry<int> b) {
      return a.annotation == b.annotation
          && _almostEqual(a.localPosition.dx, b.localPosition.dx, maxCoordinateRelativeDiff)
          && _almostEqual(a.localPosition.dy, b.localPosition.dy, maxCoordinateRelativeDiff);
    },
    'equal to',
  );
}
