// 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:async';
import 'dart:ui' as ui show ColorFilter, Image;

import 'package:fake_async/fake_async.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/painting.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:vector_math/vector_math_64.dart';

import '../image_data.dart';
import '../painting/mocks_for_image_cache.dart';
import '../rendering/rendering_tester.dart';

class TestCanvas implements Canvas {
  final List<Invocation> invocations = <Invocation>[];

  @override
  void noSuchMethod(Invocation invocation) {
    invocations.add(invocation);
  }
}

class SynchronousTestImageProvider extends ImageProvider<int> {
  const SynchronousTestImageProvider(this.image);

  final ui.Image image;

  @override
  Future<int> obtainKey(ImageConfiguration configuration) {
    return SynchronousFuture<int>(1);
  }

  @override
  ImageStreamCompleter load(int key, DecoderCallback decode) {
    return OneFrameImageStreamCompleter(
      SynchronousFuture<ImageInfo>(TestImageInfo(key, image: image)),
    );
  }
}

class SynchronousErrorTestImageProvider extends ImageProvider<int> {
  const SynchronousErrorTestImageProvider(this.throwable);

  final Object throwable;

  @override
  Future<int> obtainKey(ImageConfiguration configuration) {
    throw throwable;
  }

  @override
  ImageStreamCompleter load(int key, DecoderCallback decode) {
    throw throwable;
  }
}

class AsyncTestImageProvider extends ImageProvider<int> {
  AsyncTestImageProvider(this.image);

  final ui.Image image;

  @override
  Future<int> obtainKey(ImageConfiguration configuration) {
    return Future<int>.value(2);
  }

  @override
  ImageStreamCompleter load(int key, DecoderCallback decode) {
    return OneFrameImageStreamCompleter(
      Future<ImageInfo>.value(TestImageInfo(key, image: image)),
    );
  }
}

class DelayedImageProvider extends ImageProvider<DelayedImageProvider> {
  DelayedImageProvider(this.image);

  final ui.Image image;

  final Completer<ImageInfo> _completer = Completer<ImageInfo>();

  @override
  Future<DelayedImageProvider> obtainKey(ImageConfiguration configuration) {
    return SynchronousFuture<DelayedImageProvider>(this);
  }

  @override
  ImageStreamCompleter load(DelayedImageProvider key, DecoderCallback decode) {
    return OneFrameImageStreamCompleter(_completer.future);
  }

  Future<void> complete() async {
    _completer.complete(ImageInfo(image: image));
  }

  @override
  String toString() => '${describeIdentity(this)}()';
}

class MultiFrameImageProvider extends ImageProvider<MultiFrameImageProvider> {
  MultiFrameImageProvider(this.completer);

  final MultiImageCompleter completer;

  @override
  Future<MultiFrameImageProvider> obtainKey(ImageConfiguration configuration) {
    return SynchronousFuture<MultiFrameImageProvider>(this);
  }

  @override
  ImageStreamCompleter load(MultiFrameImageProvider key, DecoderCallback decode) {
    return completer;
  }

  @override
  String toString() => '${describeIdentity(this)}()';
}

class MultiImageCompleter extends ImageStreamCompleter {
  void testSetImage(ImageInfo info) {
    setImage(info);
  }
}

void main() {
  TestRenderingFlutterBinding.ensureInitialized();

  test('Decoration.lerp()', () {
    const BoxDecoration a = BoxDecoration(color: Color(0xFFFFFFFF));
    const BoxDecoration b = BoxDecoration(color: Color(0x00000000));

    BoxDecoration c = Decoration.lerp(a, b, 0.0)! as BoxDecoration;
    expect(c.color, equals(a.color));

    c = Decoration.lerp(a, b, 0.25)! as BoxDecoration;
    expect(c.color, equals(Color.lerp(const Color(0xFFFFFFFF), const Color(0x00000000), 0.25)));

    c = Decoration.lerp(a, b, 1.0)! as BoxDecoration;
    expect(c.color, equals(b.color));
  });

  test('Decoration equality', () {
    const BoxDecoration a = BoxDecoration(
      color: Color(0xFFFFFFFF),
      boxShadow: <BoxShadow>[BoxShadow()],
    );

    const BoxDecoration b = BoxDecoration(
      color: Color(0xFFFFFFFF),
      boxShadow: <BoxShadow>[BoxShadow()],
    );

    expect(a.hashCode, equals(b.hashCode));
    expect(a, equals(b));
  });

  test('BoxDecorationImageListenerSync', () async {
    final ui.Image image = await createTestImage(width: 100, height: 100);
    final ImageProvider imageProvider = SynchronousTestImageProvider(image);
    final DecorationImage backgroundImage = DecorationImage(image: imageProvider);

    final BoxDecoration boxDecoration = BoxDecoration(image: backgroundImage);
    bool onChangedCalled = false;
    final BoxPainter boxPainter = boxDecoration.createBoxPainter(() {
      onChangedCalled = true;
    });

    final TestCanvas canvas = TestCanvas();
    const ImageConfiguration imageConfiguration = ImageConfiguration(size: Size.zero);
    boxPainter.paint(canvas, Offset.zero, imageConfiguration);

    // The onChanged callback should not be invoked during the call to boxPainter.paint
    expect(onChangedCalled, equals(false));
  });

  test('BoxDecorationImageListenerAsync', () async {
    final ui.Image image = await createTestImage(width: 10, height: 10);
    FakeAsync().run((FakeAsync async) {
      final ImageProvider imageProvider = AsyncTestImageProvider(image);
      final DecorationImage backgroundImage = DecorationImage(image: imageProvider);

      final BoxDecoration boxDecoration = BoxDecoration(image: backgroundImage);
      bool onChangedCalled = false;
      final BoxPainter boxPainter = boxDecoration.createBoxPainter(() {
        onChangedCalled = true;
      });

      final TestCanvas canvas = TestCanvas();
      const ImageConfiguration imageConfiguration = ImageConfiguration(size: Size.zero);
      boxPainter.paint(canvas, Offset.zero, imageConfiguration);

      // The onChanged callback should be invoked asynchronously.
      expect(onChangedCalled, equals(false));
      async.flushMicrotasks();
      expect(onChangedCalled, equals(true));
    });
  });

  test('BoxDecorationImageListener does not change when image is clone', () async {
    final ui.Image image1 = await createTestImage(width: 10, height: 10, cache: false);
    final ui.Image image2 = await createTestImage(width: 10, height: 10, cache: false);
    final MultiImageCompleter completer = MultiImageCompleter();
    final MultiFrameImageProvider imageProvider = MultiFrameImageProvider(completer);
    final DecorationImage backgroundImage = DecorationImage(image: imageProvider);

    final BoxDecoration boxDecoration = BoxDecoration(image: backgroundImage);
    bool onChangedCalled = false;
    final BoxPainter boxPainter = boxDecoration.createBoxPainter(() {
      onChangedCalled = true;
    });

    final TestCanvas canvas = TestCanvas();
    const ImageConfiguration imageConfiguration = ImageConfiguration(size: Size.zero);
    boxPainter.paint(canvas, Offset.zero, imageConfiguration);

    // The onChanged callback should be invoked asynchronously.
    expect(onChangedCalled, equals(false));

    completer.testSetImage(ImageInfo(image: image1.clone()));
    await null;

    expect(onChangedCalled, equals(true));
    onChangedCalled = false;
    completer.testSetImage(ImageInfo(image: image1.clone()));
    await null;

    expect(onChangedCalled, equals(false));

    completer.testSetImage(ImageInfo(image: image2.clone()));
    await null;

    expect(onChangedCalled, equals(true));
  });

  // Regression test for https://github.com/flutter/flutter/issues/7289.
  // A reference test would be better.
  test('BoxDecoration backgroundImage clip', () async {
    final ui.Image image = await createTestImage(width: 100, height: 100);
    void testDecoration({ BoxShape shape = BoxShape.rectangle, BorderRadius? borderRadius, required bool expectClip }) {
      FakeAsync().run((FakeAsync async) async {
        final DelayedImageProvider imageProvider = DelayedImageProvider(image);
        final DecorationImage backgroundImage = DecorationImage(image: imageProvider);

        final BoxDecoration boxDecoration = BoxDecoration(
          shape: shape,
          borderRadius: borderRadius,
          image: backgroundImage,
        );

        final TestCanvas canvas = TestCanvas();
        const ImageConfiguration imageConfiguration = ImageConfiguration(
          size: Size(100.0, 100.0),
        );
        bool onChangedCalled = false;
        final BoxPainter boxPainter = boxDecoration.createBoxPainter(() {
          onChangedCalled = true;
        });

        // _BoxDecorationPainter._paintDecorationImage() resolves the background
        // image and adds a listener to the resolved image stream.
        boxPainter.paint(canvas, Offset.zero, imageConfiguration);
        await imageProvider.complete();

        // Run the listener which calls onChanged() which saves an internal
        // reference to the TestImage.
        async.flushMicrotasks();
        expect(onChangedCalled, isTrue);
        boxPainter.paint(canvas, Offset.zero, imageConfiguration);

        // We expect a clip to precede the drawImageRect call.
        final List<Invocation> commands = canvas.invocations.where((Invocation invocation) {
          return invocation.memberName == #clipPath || invocation.memberName == #drawImageRect;
        }).toList();
        if (expectClip) { // We expect a clip to precede the drawImageRect call.
          expect(commands.length, 2);
          expect(commands[0].memberName, equals(#clipPath));
          expect(commands[1].memberName, equals(#drawImageRect));
        } else {
          expect(commands.length, 1);
          expect(commands[0].memberName, equals(#drawImageRect));
        }
      });
    }

    testDecoration(shape: BoxShape.circle, expectClip: true);
    testDecoration(borderRadius: const BorderRadius.all(Radius.circular(16.0)), expectClip: true);
    testDecoration(expectClip: false);
  });

  test('DecorationImage test', () async {
    const ColorFilter colorFilter = ui.ColorFilter.mode(Color(0xFF00FF00), BlendMode.src);
    final ui.Image image = await createTestImage(width: 100, height: 100);
    final DecorationImage backgroundImage = DecorationImage(
      image: SynchronousTestImageProvider(image),
      colorFilter: colorFilter,
      fit: BoxFit.contain,
      alignment: Alignment.bottomLeft,
      centerSlice: const Rect.fromLTWH(10.0, 20.0, 30.0, 40.0),
      repeat: ImageRepeat.repeatY,
      opacity: 0.5,
      filterQuality: FilterQuality.high,
      invertColors: true,
      isAntiAlias: true,
    );

    final BoxDecoration boxDecoration = BoxDecoration(image: backgroundImage);
    final BoxPainter boxPainter = boxDecoration.createBoxPainter(() { assert(false); });
    final TestCanvas canvas = TestCanvas();
    boxPainter.paint(canvas, Offset.zero, const ImageConfiguration(size: Size(100.0, 100.0)));

    final Invocation call = canvas.invocations.singleWhere((Invocation call) => call.memberName == #drawImageNine);
    expect(call.isMethod, isTrue);
    expect(call.positionalArguments, hasLength(4));
    expect(call.positionalArguments[0], isA<ui.Image>());
    expect(call.positionalArguments[1], const Rect.fromLTRB(10.0, 20.0, 40.0, 60.0));
    expect(call.positionalArguments[2], const Rect.fromLTRB(0.0, 0.0, 100.0, 100.0));
    expect(call.positionalArguments[3], isA<Paint>());
    final Paint paint = call.positionalArguments[3] as Paint;
    expect(paint.colorFilter, colorFilter);
    expect(paint.color, const Color(0x7F000000)); // 0.5 opacity
    expect(paint.filterQuality, FilterQuality.high);
    expect(paint.isAntiAlias, true);
    // TODO(craiglabenz): change to true when https://github.com/flutter/flutter/issues/88909 is fixed
    expect(paint.invertColors, !kIsWeb);
  });

  test('DecorationImage with null textDirection configuration should throw Error', () async {
    const ColorFilter colorFilter = ui.ColorFilter.mode(Color(0xFF00FF00), BlendMode.src);
    final ui.Image image = await createTestImage(width: 100, height: 100);
    final DecorationImage backgroundImage = DecorationImage(
      image: SynchronousTestImageProvider(image),
      colorFilter: colorFilter,
      fit: BoxFit.contain,
      centerSlice: const Rect.fromLTWH(10.0, 20.0, 30.0, 40.0),
      repeat: ImageRepeat.repeatY,
      matchTextDirection: true,
      scale: 0.5,
      opacity: 0.5,
      invertColors: true,
      isAntiAlias: true,
    );
    final BoxDecoration boxDecoration = BoxDecoration(image: backgroundImage);
    final BoxPainter boxPainter = boxDecoration.createBoxPainter(() {
      assert(false);
    });
    final TestCanvas canvas = TestCanvas();
    late FlutterError error;
    try {
      boxPainter.paint(canvas, Offset.zero, const ImageConfiguration(
        size: Size(100.0, 100.0),
      ));
    } on FlutterError catch (e) {
      error = e;
    }
    expect(error, isNotNull);
    expect(error.diagnostics.length, 4);
    expect(error.diagnostics[2], isA<DiagnosticsProperty<DecorationImage>>());
    expect(error.diagnostics[3], isA<DiagnosticsProperty<ImageConfiguration>>());
    expect(error.toStringDeep(),
      'FlutterError\n'
      '   DecorationImage.matchTextDirection can only be used when a\n'
      '   TextDirection is available.\n'
      '   When DecorationImagePainter.paint() was called, there was no text\n'
      '   direction provided in the ImageConfiguration object to match.\n'
      '   The DecorationImage was:\n'
      '     DecorationImage(SynchronousTestImageProvider(),\n'
      '     ColorFilter.mode(Color(0xff00ff00), BlendMode.src),\n'
      '     BoxFit.contain, Alignment.center, centerSlice:\n'
      '     Rect.fromLTRB(10.0, 20.0, 40.0, 60.0), ImageRepeat.repeatY,\n'
      '     match text direction, scale 0.5, opacity 0.5,\n'
      '     FilterQuality.low, invert colors, use anti-aliasing)\n'
      '   The ImageConfiguration was:\n'
      '     ImageConfiguration(size: Size(100.0, 100.0))\n',
    );
  }, skip: kIsWeb); // https://github.com/flutter/flutter/issues/87364

  test('DecorationImage - error listener', () async {
    late String exception;
    final DecorationImage backgroundImage = DecorationImage(
      image: const SynchronousErrorTestImageProvider('threw'),
      onError: (dynamic error, StackTrace? stackTrace) {
        exception = error as String;
      },
    );

    backgroundImage.createPainter(() { }).paint(
      TestCanvas(),
      Rect.largest,
      Path(),
      ImageConfiguration.empty,
    );
    // Yield so that the exception callback gets called before we check it.
    await null;
    expect(exception, 'threw');
  });

  test('BoxDecoration.lerp - shapes', () {
    // We don't lerp the shape, we just switch from one to the other at t=0.5.
    // (Use a ShapeDecoration and ShapeBorder if you want to lerp the shapes...)
    expect(
      BoxDecoration.lerp(
        const BoxDecoration(),
        const BoxDecoration(shape: BoxShape.circle),
        -1.0,
      ),
      const BoxDecoration(),
    );
    expect(
      BoxDecoration.lerp(
        const BoxDecoration(),
        const BoxDecoration(shape: BoxShape.circle),
        0.0,
      ),
      const BoxDecoration(),
    );
    expect(
      BoxDecoration.lerp(
        const BoxDecoration(),
        const BoxDecoration(shape: BoxShape.circle),
        0.25,
      ),
      const BoxDecoration(),
    );
    expect(
      BoxDecoration.lerp(
        const BoxDecoration(),
        const BoxDecoration(shape: BoxShape.circle),
        0.75,
      ),
      const BoxDecoration(shape: BoxShape.circle),
    );
    expect(
      BoxDecoration.lerp(
        const BoxDecoration(),
        const BoxDecoration(shape: BoxShape.circle),
        1.0,
      ),
      const BoxDecoration(shape: BoxShape.circle),
    );
    expect(
      BoxDecoration.lerp(
        const BoxDecoration(),
        const BoxDecoration(shape: BoxShape.circle),
        2.0,
      ),
      const BoxDecoration(shape: BoxShape.circle),
    );
  });

  test('BoxDecoration.lerp - gradients', () {
    const Gradient gradient = LinearGradient(colors: <Color>[ Color(0x00000000), Color(0xFFFFFFFF) ]);
    expect(
      BoxDecoration.lerp(
        const BoxDecoration(),
        const BoxDecoration(gradient: gradient),
        -1.0,
      ),
      const BoxDecoration(gradient: LinearGradient(colors: <Color>[ Color(0x00000000), Color(0x00FFFFFF) ])),
    );
    expect(
      BoxDecoration.lerp(
        const BoxDecoration(),
        const BoxDecoration(gradient: gradient),
        0.0,
      ),
      const BoxDecoration(),
    );
    expect(
      BoxDecoration.lerp(
        const BoxDecoration(),
        const BoxDecoration(gradient: gradient),
        0.25,
      ),
      const BoxDecoration(gradient: LinearGradient(colors: <Color>[ Color(0x00000000), Color(0x40FFFFFF) ])),
    );
    expect(
      BoxDecoration.lerp(
        const BoxDecoration(),
        const BoxDecoration(gradient: gradient),
        0.75,
      ),
      const BoxDecoration(gradient: LinearGradient(colors: <Color>[ Color(0x00000000), Color(0xBFFFFFFF) ])),
    );
    expect(
      BoxDecoration.lerp(
        const BoxDecoration(),
        const BoxDecoration(gradient: gradient),
        1.0,
      ),
      const BoxDecoration(gradient: gradient),
    );
    expect(
      BoxDecoration.lerp(
        const BoxDecoration(),
        const BoxDecoration(gradient: gradient),
        2.0,
      ),
      const BoxDecoration(gradient: gradient),
    );
  });

  test('Decoration.lerp with unrelated decorations', () {
    expect(Decoration.lerp(const FlutterLogoDecoration(), const BoxDecoration(), 0.0), isA<FlutterLogoDecoration>());
    expect(Decoration.lerp(const FlutterLogoDecoration(), const BoxDecoration(), 0.25), isA<FlutterLogoDecoration>());
    expect(Decoration.lerp(const FlutterLogoDecoration(), const BoxDecoration(), 0.75), isA<BoxDecoration>());
    expect(Decoration.lerp(const FlutterLogoDecoration(), const BoxDecoration(), 1.0), isA<BoxDecoration>());
  });

  test('paintImage BoxFit.none scale test', () async {
    for (double scale = 1.0; scale <= 4.0; scale += 1.0) {
      final TestCanvas canvas = TestCanvas();

      const Rect outputRect = Rect.fromLTWH(30.0, 30.0, 250.0, 250.0);
      final ui.Image image = await createTestImage(width: 100, height: 100);

      paintImage(
        canvas: canvas,
        rect: outputRect,
        image: image,
        scale: scale,
        alignment: Alignment.bottomRight,
        fit: BoxFit.none,
      );

      const Size imageSize = Size(100.0, 100.0);

      final Invocation call = canvas.invocations.firstWhere((Invocation call) => call.memberName == #drawImageRect);

      expect(call.isMethod, isTrue);
      expect(call.positionalArguments, hasLength(4));

      expect(call.positionalArguments[0], isA<ui.Image>());

      // sourceRect should contain all pixels of the source image
      expect(call.positionalArguments[1], Offset.zero & imageSize);

      // Image should be scaled down (divided by scale)
      // and be positioned in the bottom right of the outputRect
      final Size expectedTileSize = imageSize / scale;
      final Rect expectedTileRect = Rect.fromPoints(
        outputRect.bottomRight.translate(-expectedTileSize.width, -expectedTileSize.height),
        outputRect.bottomRight,
      );
      expect(call.positionalArguments[2], expectedTileRect);

      expect(call.positionalArguments[3], isA<Paint>());
    }
  });

  test('paintImage BoxFit.scaleDown scale test', () async {
    for (double scale = 1.0; scale <= 4.0; scale += 1.0) {
      final TestCanvas canvas = TestCanvas();

      // container size > scaled image size
      const Rect outputRect = Rect.fromLTWH(30.0, 30.0, 250.0, 250.0);
      final ui.Image image = await createTestImage(width: 100, height: 100);

      paintImage(
        canvas: canvas,
        rect: outputRect,
        image: image,
        scale: scale,
        alignment: Alignment.bottomRight,
        fit: BoxFit.scaleDown,
      );

      const Size imageSize = Size(100.0, 100.0);

      final Invocation call = canvas.invocations.firstWhere((Invocation call) => call.memberName == #drawImageRect);

      expect(call.isMethod, isTrue);
      expect(call.positionalArguments, hasLength(4));

      expect(call.positionalArguments[0], isA<ui.Image>());

      // sourceRect should contain all pixels of the source image
      expect(call.positionalArguments[1], Offset.zero & imageSize);

      // Image should be scaled down (divided by scale)
      // and be positioned in the bottom right of the outputRect
      final Size expectedTileSize = imageSize / scale;
      final Rect expectedTileRect = Rect.fromPoints(
        outputRect.bottomRight.translate(-expectedTileSize.width, -expectedTileSize.height),
        outputRect.bottomRight,
      );
      expect(call.positionalArguments[2], expectedTileRect);

      expect(call.positionalArguments[3], isA<Paint>());
    }
  });

  test('paintImage BoxFit.scaleDown test', () async {
    final TestCanvas canvas = TestCanvas();

    // container height (20 px) < scaled image height (50 px)
    const Rect outputRect = Rect.fromLTWH(30.0, 30.0, 250.0, 20.0);
    final ui.Image image = await createTestImage(width: 100, height: 100);

    paintImage(
      canvas: canvas,
      rect: outputRect,
      image: image,
      scale: 2.0,
      alignment: Alignment.bottomRight,
      fit: BoxFit.scaleDown,
    );

    const Size imageSize = Size(100.0, 100.0);

    final Invocation call = canvas.invocations.firstWhere((Invocation call) => call.memberName == #drawImageRect);

    expect(call.isMethod, isTrue);
    expect(call.positionalArguments, hasLength(4));

    expect(call.positionalArguments[0], isA<ui.Image>());

    // sourceRect should contain all pixels of the source image
    expect(call.positionalArguments[1], Offset.zero & imageSize);

    // Image should be scaled down to fit in height
    // and be positioned in the bottom right of the outputRect
    const Size expectedTileSize = Size(20.0, 20.0);
    final Rect expectedTileRect = Rect.fromPoints(
      outputRect.bottomRight.translate(-expectedTileSize.width, -expectedTileSize.height),
      outputRect.bottomRight,
    );
    expect(call.positionalArguments[2], expectedTileRect);

    expect(call.positionalArguments[3], isA<Paint>());
  });

  test('paintImage boxFit, scale and alignment test', () async {
    const List<BoxFit> boxFits = <BoxFit>[
      BoxFit.contain,
      BoxFit.cover,
      BoxFit.fitWidth,
      BoxFit.fitWidth,
      BoxFit.fitHeight,
      BoxFit.none,
      BoxFit.scaleDown,
    ];

    for (final BoxFit boxFit in boxFits) {
      final TestCanvas canvas = TestCanvas();

      const Rect outputRect = Rect.fromLTWH(30.0, 30.0, 250.0, 250.0);
      final ui.Image image = await createTestImage(width: 100, height: 100);

      paintImage(
        canvas: canvas,
        rect: outputRect,
        image: image,
        scale: 3.0,
        fit: boxFit,
      );

      final Invocation call = canvas.invocations.firstWhere((Invocation call) => call.memberName == #drawImageRect);

      expect(call.isMethod, isTrue);
      expect(call.positionalArguments, hasLength(4));

      // Image should be positioned in the center of the container
      // ignore: avoid_dynamic_calls
      expect(call.positionalArguments[2].center, outputRect.center);
    }
  });

  test('paintImage with repeatX and fitHeight', () async {
    final TestCanvas canvas = TestCanvas();

    // Paint a square image into an output rect that is twice as wide as it is
    // tall. One copy of the image should be painted, aligned so that a repeating
    // tile mode causes it to appear twice.
    const Rect outputRect = Rect.fromLTWH(30.0, 30.0, 400.0, 200.0);
    final ui.Image image = await createTestImage(width: 100, height: 100);

    paintImage(
      canvas: canvas,
      rect: outputRect,
      image: image,
      alignment: Alignment.topLeft,
      fit: BoxFit.fitHeight,
      repeat: ImageRepeat.repeatX,
    );

    final List<Invocation> calls = canvas.invocations.where((Invocation call) => call.memberName == #drawRect).toList();

    expect(calls, hasLength(1));
    final Invocation call = calls[0];
    expect(call.isMethod, isTrue);
    expect(call.positionalArguments, hasLength(2));

    // A tiled image is drawn as a rect with a shader.
    expect(call.positionalArguments[0], isA<Rect>());
    expect(call.positionalArguments[1], isA<Paint>());

    final Paint paint = call.positionalArguments[1] as Paint;

    expect(paint.shader, isA<ImageShader>());
    expect(call.positionalArguments[0], outputRect);
  });

  test('paintImage with repeatY and fitWidth', () async {
    final TestCanvas canvas = TestCanvas();

    // Paint a square image into an output rect that is twice as tall as it is
    // wide.  One copy of the image should be painted, aligned so that a repeating
    // tile mode causes it to appear twice.
    const Rect outputRect = Rect.fromLTWH(30.0, 30.0, 200.0, 400.0);
    final ui.Image image = await createTestImage(width: 100, height: 100);

    paintImage(
      canvas: canvas,
      rect: outputRect,
      image: image,
      alignment: Alignment.topLeft,
      fit: BoxFit.fitWidth,
      repeat: ImageRepeat.repeatY,
    );
    final List<Invocation> calls = canvas.invocations.where((Invocation call) => call.memberName == #drawRect).toList();

    expect(calls, hasLength(1));
    final Invocation call = calls[0];
    expect(call.isMethod, isTrue);
    expect(call.positionalArguments, hasLength(2));

    // A tiled image is drawn as a rect with a shader.
    expect(call.positionalArguments[0], isA<Rect>());
    expect(call.positionalArguments[1], isA<Paint>());

    final Paint paint = call.positionalArguments[1] as Paint;

    expect(paint.shader, isA<ImageShader>());
    expect(call.positionalArguments[0], outputRect);
  });

  test('DecorationImage scale test', () async {
    final ui.Image image = await createTestImage(width: 100, height: 100);
    final DecorationImage backgroundImage = DecorationImage(
      image: SynchronousTestImageProvider(image),
      scale: 4,
      alignment: Alignment.topLeft,
    );

    final BoxDecoration boxDecoration = BoxDecoration(image: backgroundImage);
    final BoxPainter boxPainter = boxDecoration.createBoxPainter(() { assert(false); });
    final TestCanvas canvas = TestCanvas();
    boxPainter.paint(canvas, Offset.zero, const ImageConfiguration(size: Size(100.0, 100.0)));

    final Invocation call = canvas.invocations.firstWhere((Invocation call) => call.memberName == #drawImageRect);
    // The image should scale down to Size(25.0, 25.0) from Size(100.0, 100.0)
    // considering DecorationImage scale to be 4.0 and Image scale to be 1.0.
    // ignore: avoid_dynamic_calls
    expect(call.positionalArguments[2].size, const Size(25.0, 25.0));
    expect(call.positionalArguments[2], const Rect.fromLTRB(0.0, 0.0, 25.0, 25.0));
  });

  test('DecorationImagePainter disposes of image when disposed',  () async {
    final ImageProvider provider = MemoryImage(Uint8List.fromList(kTransparentImage));

    final ImageStream stream = provider.resolve(ImageConfiguration.empty);

    final Completer<ImageInfo> infoCompleter = Completer<ImageInfo>();
    void listener(ImageInfo image, bool syncCall) {
      assert(!infoCompleter.isCompleted);
      infoCompleter.complete(image);
    }
    stream.addListener(ImageStreamListener(listener));

    final ImageInfo info = await infoCompleter.future;
    final int baselineRefCount = info.image.debugGetOpenHandleStackTraces()!.length;

    final DecorationImagePainter painter = DecorationImage(image: provider).createPainter(() {});
    final Canvas canvas = TestCanvas();
    painter.paint(canvas, Rect.zero, Path(), ImageConfiguration.empty);

    expect(info.image.debugGetOpenHandleStackTraces()!.length, baselineRefCount + 1);
    painter.dispose();
    expect(info.image.debugGetOpenHandleStackTraces()!.length, baselineRefCount);

    info.dispose();
  }, skip: kIsWeb); // https://github.com/flutter/flutter/issues/87442

  test('Compute image tiling', () {
    expect(() => createTilingInfo(ImageRepeat.noRepeat, Rect.zero, Rect.zero, Rect.zero), throwsAssertionError);

    // These tests draw a 16x9 image into a 100x50 container with a destination
    // size of and make assertions based on observed behavior and the original
    // rectangles from https://github.com/flutter/flutter/pull/119495/

    final ImageTilingInfo repeatX = createTilingInfo(
      ImageRepeat.repeatX,
      const Rect.fromLTRB(0.0, 0.0, 100.0, 50.0),
      const Rect.fromLTRB(84.0, 0.0, 100.0, 9.0),
      const Rect.fromLTRB(0.0, 0.0, 16.0, 9.0),
    );

    expect(repeatX.tmx, TileMode.repeated);
    expect(repeatX.tmy, TileMode.decal);
    expect(repeatX.transform, matrixMoreOrLessEquals(Matrix4.identity()
      ..scale(1.0, 1.0)
      ..setTranslationRaw(-12.0, 0.0, 0.0)
    ));

    final ImageTilingInfo repeatY = createTilingInfo(
      ImageRepeat.repeatY,
      const Rect.fromLTRB(0.0, 0.0, 100.0, 50.0),
      const Rect.fromLTRB(84.0, 0.0, 100.0, 9.0),
      const Rect.fromLTRB(0.0, 0.0, 16.0, 9.0),
    );
    expect(repeatY.tmx, TileMode.decal);
    expect(repeatY.tmy, TileMode.repeated);
    expect(repeatY.transform, matrixMoreOrLessEquals(Matrix4.identity()
      ..scale(1.0, 1.0)
      ..setTranslationRaw(84.0, 0.0, 0.0)
    ));

    final ImageTilingInfo repeat = createTilingInfo(
      ImageRepeat.repeat,
      const Rect.fromLTRB(0.0, 0.0, 100.0, 50.0),
      const Rect.fromLTRB(84.0, 0.0, 100.0, 9.0),
      const Rect.fromLTRB(0.0, 0.0, 16.0, 9.0),
    );
    expect(repeat.tmx, TileMode.repeated);
    expect(repeat.tmy, TileMode.repeated);
    expect(repeat.transform, matrixMoreOrLessEquals(Matrix4.identity()
      ..scale(1.0, 1.0)
      ..setTranslationRaw(-12.0, 0.0, 0.0)
    ));
  });
}
