// Copyright 2013 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:js_interop';
import 'dart:math';
import 'dart:typed_data';

import 'package:test/bootstrap/browser.dart';
import 'package:test/test.dart';

import 'package:ui/src/engine.dart';
import 'package:ui/ui.dart' as ui;
import 'package:web_engine_tester/golden_tester.dart';

import '../common/matchers.dart';
import 'common.dart';
import 'test_data.dart';

void main() {
  internalBootstrapBrowserTest(() => testMain);
}

void testMain() {
  group('CanvasKit API', () {
    setUpCanvasKitTest();

    _blendModeTests();
    _paintStyleTests();
    _strokeCapTests();
    _strokeJoinTests();
    _blurStyleTests();
    _tileModeTests();
    _fillTypeTests();
    _pathOpTests();
    _clipOpTests();
    _pointModeTests();
    _vertexModeTests();
    _imageTests();
    _shaderTests();
    _paintTests();
    _maskFilterTests();
    _colorFilterTests();
    _imageFilterTests();
    _mallocTests();
    _sharedColorTests();
    _toSkPointTests();
    _toSkColorStopsTests();
    _toSkMatrixFromFloat32Tests();
    _toSkM44FromFloat32Tests();
    _matrix4x4CompositionTests();
    _toSkRectTests();
    _skVerticesTests();
    _pictureTests();
    group('SkParagraph', () {
      _paragraphTests();
    });
    group('SkPath', () {
      _pathTests();
    });
    group('SkCanvas', () {
      _canvasTests();
    });
    group('SkParagraph', () {
      _textStyleTests();
    });
  });
}

void _blendModeTests() {
  test('blend mode mapping is correct', () {
    expect(canvasKit.BlendMode.Clear.value, ui.BlendMode.clear.index);
    expect(canvasKit.BlendMode.Src.value, ui.BlendMode.src.index);
    expect(canvasKit.BlendMode.Dst.value, ui.BlendMode.dst.index);
    expect(canvasKit.BlendMode.SrcOver.value, ui.BlendMode.srcOver.index);
    expect(canvasKit.BlendMode.DstOver.value, ui.BlendMode.dstOver.index);
    expect(canvasKit.BlendMode.SrcIn.value, ui.BlendMode.srcIn.index);
    expect(canvasKit.BlendMode.DstIn.value, ui.BlendMode.dstIn.index);
    expect(canvasKit.BlendMode.SrcOut.value, ui.BlendMode.srcOut.index);
    expect(canvasKit.BlendMode.DstOut.value, ui.BlendMode.dstOut.index);
    expect(canvasKit.BlendMode.SrcATop.value, ui.BlendMode.srcATop.index);
    expect(canvasKit.BlendMode.DstATop.value, ui.BlendMode.dstATop.index);
    expect(canvasKit.BlendMode.Xor.value, ui.BlendMode.xor.index);
    expect(canvasKit.BlendMode.Plus.value, ui.BlendMode.plus.index);
    expect(canvasKit.BlendMode.Modulate.value, ui.BlendMode.modulate.index);
    expect(canvasKit.BlendMode.Screen.value, ui.BlendMode.screen.index);
    expect(canvasKit.BlendMode.Overlay.value, ui.BlendMode.overlay.index);
    expect(canvasKit.BlendMode.Darken.value, ui.BlendMode.darken.index);
    expect(canvasKit.BlendMode.Lighten.value, ui.BlendMode.lighten.index);
    expect(canvasKit.BlendMode.ColorDodge.value, ui.BlendMode.colorDodge.index);
    expect(canvasKit.BlendMode.ColorBurn.value, ui.BlendMode.colorBurn.index);
    expect(canvasKit.BlendMode.HardLight.value, ui.BlendMode.hardLight.index);
    expect(canvasKit.BlendMode.SoftLight.value, ui.BlendMode.softLight.index);
    expect(canvasKit.BlendMode.Difference.value, ui.BlendMode.difference.index);
    expect(canvasKit.BlendMode.Exclusion.value, ui.BlendMode.exclusion.index);
    expect(canvasKit.BlendMode.Multiply.value, ui.BlendMode.multiply.index);
    expect(canvasKit.BlendMode.Hue.value, ui.BlendMode.hue.index);
    expect(canvasKit.BlendMode.Saturation.value, ui.BlendMode.saturation.index);
    expect(canvasKit.BlendMode.Color.value, ui.BlendMode.color.index);
    expect(canvasKit.BlendMode.Luminosity.value, ui.BlendMode.luminosity.index);
  });

  test('ui.BlendMode converts to SkBlendMode', () {
    for (final ui.BlendMode blendMode in ui.BlendMode.values) {
      expect(toSkBlendMode(blendMode).value, blendMode.index);
    }
  });
}

void _paintStyleTests() {
  test('paint style mapping is correct', () {
    expect(canvasKit.PaintStyle.Fill.value, ui.PaintingStyle.fill.index);
    expect(canvasKit.PaintStyle.Stroke.value, ui.PaintingStyle.stroke.index);
  });

  test('ui.PaintingStyle converts to SkPaintStyle', () {
    for (final ui.PaintingStyle style in ui.PaintingStyle.values) {
      expect(toSkPaintStyle(style).value, style.index);
    }
  });
}

void _strokeCapTests() {
  test('stroke cap mapping is correct', () {
    expect(canvasKit.StrokeCap.Butt.value, ui.StrokeCap.butt.index);
    expect(canvasKit.StrokeCap.Round.value, ui.StrokeCap.round.index);
    expect(canvasKit.StrokeCap.Square.value, ui.StrokeCap.square.index);
  });

  test('ui.StrokeCap converts to SkStrokeCap', () {
    for (final ui.StrokeCap cap in ui.StrokeCap.values) {
      expect(toSkStrokeCap(cap).value, cap.index);
    }
  });
}

void _strokeJoinTests() {
  test('stroke cap mapping is correct', () {
    expect(canvasKit.StrokeJoin.Miter.value, ui.StrokeJoin.miter.index);
    expect(canvasKit.StrokeJoin.Round.value, ui.StrokeJoin.round.index);
    expect(canvasKit.StrokeJoin.Bevel.value, ui.StrokeJoin.bevel.index);
  });

  test('ui.StrokeJoin converts to SkStrokeJoin', () {
    for (final ui.StrokeJoin join in ui.StrokeJoin.values) {
      expect(toSkStrokeJoin(join).value, join.index);
    }
  });
}

void _blurStyleTests() {
  test('blur style mapping is correct', () {
    expect(canvasKit.BlurStyle.Normal.value, ui.BlurStyle.normal.index);
    expect(canvasKit.BlurStyle.Solid.value, ui.BlurStyle.solid.index);
    expect(canvasKit.BlurStyle.Outer.value, ui.BlurStyle.outer.index);
    expect(canvasKit.BlurStyle.Inner.value, ui.BlurStyle.inner.index);
  });

  test('ui.BlurStyle converts to SkBlurStyle', () {
    for (final ui.BlurStyle style in ui.BlurStyle.values) {
      expect(toSkBlurStyle(style).value, style.index);
    }
  });
}

void _tileModeTests() {
  test('tile mode mapping is correct', () {
    expect(canvasKit.TileMode.Clamp.value, ui.TileMode.clamp.index);
    expect(canvasKit.TileMode.Repeat.value, ui.TileMode.repeated.index);
    expect(canvasKit.TileMode.Mirror.value, ui.TileMode.mirror.index);
  });

  test('ui.TileMode converts to SkTileMode', () {
    for (final ui.TileMode mode in ui.TileMode.values) {
      expect(toSkTileMode(mode).value, mode.index);
    }
  });
}

void _fillTypeTests() {
  test('fill type mapping is correct', () {
    expect(canvasKit.FillType.Winding.value, ui.PathFillType.nonZero.index);
    expect(canvasKit.FillType.EvenOdd.value, ui.PathFillType.evenOdd.index);
  });

  test('ui.PathFillType converts to SkFillType', () {
    for (final ui.PathFillType type in ui.PathFillType.values) {
      expect(toSkFillType(type).value, type.index);
    }
  });
}

void _pathOpTests() {
  test('path op mapping is correct', () {
    expect(
        canvasKit.PathOp.Difference.value, ui.PathOperation.difference.index);
    expect(canvasKit.PathOp.Intersect.value, ui.PathOperation.intersect.index);
    expect(canvasKit.PathOp.Union.value, ui.PathOperation.union.index);
    expect(canvasKit.PathOp.XOR.value, ui.PathOperation.xor.index);
    expect(canvasKit.PathOp.ReverseDifference.value,
        ui.PathOperation.reverseDifference.index);
  });

  test('ui.PathOperation converts to SkPathOp', () {
    for (final ui.PathOperation op in ui.PathOperation.values) {
      expect(toSkPathOp(op).value, op.index);
    }
  });

  test('Path.combine test', () {
    final ui.Path path1 = ui.Path();
    expect(path1, isA<CkPath>());
    path1.addRect(const ui.Rect.fromLTRB(0, 0, 10, 10));
    path1.addOval(const ui.Rect.fromLTRB(10, 10, 100, 100));

    final ui.Path path2 = ui.Path();
    expect(path2, isA<CkPath>());
    path2.addRect(const ui.Rect.fromLTRB(5, 5, 15, 15));
    path2.addOval(const ui.Rect.fromLTRB(15, 15, 105, 105));

    final ui.Path union = ui.Path.combine(ui.PathOperation.union, path1, path2);
    expect(union, isA<CkPath>());
    expect(union.getBounds(), const ui.Rect.fromLTRB(0, 0, 105, 105));

    // Smoke-test other operations.
    for (final ui.PathOperation operation in ui.PathOperation.values) {
      final ui.Path combined = ui.Path.combine(operation, path1, path2);
      expect(combined, isA<CkPath>());
    }
  });
}

void _clipOpTests() {
  test('clip op mapping is correct', () {
    expect(canvasKit.ClipOp.Difference.value, ui.ClipOp.difference.index);
    expect(canvasKit.ClipOp.Intersect.value, ui.ClipOp.intersect.index);
  });

  test('ui.ClipOp converts to SkClipOp', () {
    for (final ui.ClipOp op in ui.ClipOp.values) {
      expect(toSkClipOp(op).value, op.index);
    }
  });
}

void _pointModeTests() {
  test('point mode mapping is correct', () {
    expect(canvasKit.PointMode.Points.value, ui.PointMode.points.index);
    expect(canvasKit.PointMode.Lines.value, ui.PointMode.lines.index);
    expect(canvasKit.PointMode.Polygon.value, ui.PointMode.polygon.index);
  });

  test('ui.PointMode converts to SkPointMode', () {
    for (final ui.PointMode op in ui.PointMode.values) {
      expect(toSkPointMode(op).value, op.index);
    }
  });
}

void _vertexModeTests() {
  test('vertex mode mapping is correct', () {
    expect(canvasKit.VertexMode.Triangles.value, ui.VertexMode.triangles.index);
    expect(canvasKit.VertexMode.TrianglesStrip.value,
        ui.VertexMode.triangleStrip.index);
    expect(canvasKit.VertexMode.TriangleFan.value,
        ui.VertexMode.triangleFan.index);
  });

  test('ui.VertexMode converts to SkVertexMode', () {
    for (final ui.VertexMode op in ui.VertexMode.values) {
      expect(toSkVertexMode(op).value, op.index);
    }
  });
}

void _imageTests() {
  test('MakeAnimatedImageFromEncoded makes a non-animated image', () {
    final SkAnimatedImage nonAnimated =
        canvasKit.MakeAnimatedImageFromEncoded(kTransparentImage)!;
    expect(nonAnimated.getFrameCount(), 1);
    expect(nonAnimated.getRepetitionCount(), 0);
    expect(nonAnimated.width(), 1);
    expect(nonAnimated.height(), 1);

    final SkImage frame = nonAnimated.makeImageAtCurrentFrame();
    expect(frame.width(), 1);
    expect(frame.height(), 1);

    expect(nonAnimated.decodeNextFrame(), -1);
    expect(
      frame.makeShaderOptions(
        canvasKit.TileMode.Repeat,
        canvasKit.TileMode.Mirror,
        canvasKit.FilterMode.Linear,
        canvasKit.MipmapMode.Nearest,
        toSkMatrixFromFloat32(Matrix4.identity().storage),
      ),
      isNotNull,
    );
  });

  test('MakeAnimatedImageFromEncoded makes an animated image', () {
    final SkAnimatedImage animated =
        canvasKit.MakeAnimatedImageFromEncoded(kAnimatedGif)!;
    expect(animated.getFrameCount(), 3);
    expect(animated.getRepetitionCount(), -1); // animates forever
    expect(animated.width(), 1);
    expect(animated.height(), 1);
    for (int i = 0; i < 100; i++) {
      final SkImage frame = animated.makeImageAtCurrentFrame();
      expect(frame.width(), 1);
      expect(frame.height(), 1);
      expect(animated.decodeNextFrame(), 100);
    }
  });
}

void _shaderTests() {
  test('MakeLinearGradient', () {
    expect(_makeTestShader(), isNotNull);
  });

  test('MakeRadialGradient', () {
    expect(
        canvasKit.Shader.MakeRadialGradient(
          Float32List.fromList(<double>[1, 1]),
          10.0,
          Uint32List.fromList(<int>[0xff000000, 0xffffffff]),
          Float32List.fromList(<double>[0, 1]),
          canvasKit.TileMode.Repeat,
          toSkMatrixFromFloat32(Matrix4.identity().storage),
          0,
        ),
        isNotNull);
  });

  test('MakeTwoPointConicalGradient', () {
    expect(
        canvasKit.Shader.MakeTwoPointConicalGradient(
          Float32List.fromList(<double>[1, 1]),
          10.0,
          Float32List.fromList(<double>[1, 1]),
          10.0,
          Uint32List.fromList(<int>[0xff000000, 0xffffffff]),
          Float32List.fromList(<double>[0, 1]),
          canvasKit.TileMode.Repeat,
          toSkMatrixFromFloat32(Matrix4.identity().storage),
          0,
        ),
        isNotNull);
  });

  test('RuntimeEffect', () {
    const String kSkSlProgram = r'''
half4 main(vec2 fragCoord) {
  return vec4(1.0, 0.0, 0.0, 1.0);
}
  ''';

    final SkRuntimeEffect? effect = MakeRuntimeEffect(kSkSlProgram);
    expect(effect, isNotNull);

    const String kInvalidSkSlProgram = '';

    // Invalid SkSL returns null.
    final SkRuntimeEffect? invalidEffect = MakeRuntimeEffect(kInvalidSkSlProgram);
    expect(invalidEffect, isNull);

    final SkShader? shader = effect!.makeShader(<double>[]);
    expect(shader, isNotNull);

    // mismatched uniforms returns null.
    final SkShader? invalidShader = effect.makeShader(<double>[1]);

    expect(invalidShader, isNull);

    const String kSkSlProgramWithUniforms = r'''
uniform vec4 u_color;

half4 main(vec2 fragCoord) {
return u_color;
}
''';

    final SkShader? shaderWithUniform = MakeRuntimeEffect(kSkSlProgramWithUniforms)
      !.makeShader(<double>[1.0, 0.0, 0.0, 1.0]);

    expect(shaderWithUniform, isNotNull);
  });
}

SkShader _makeTestShader() {
  return canvasKit.Shader.MakeLinearGradient(
    Float32List.fromList(<double>[0, 0]),
    Float32List.fromList(<double>[1, 1]),
    Uint32List.fromList(<int>[0xff0000ff]),
    Float32List.fromList(<double>[0, 1]),
    canvasKit.TileMode.Repeat,
    null,
  );
}

void _paintTests() {
  test('can make SkPaint', () async {
    final SkPaint paint = SkPaint();
    paint.setBlendMode(canvasKit.BlendMode.SrcOut);
    paint.setStyle(canvasKit.PaintStyle.Stroke);
    paint.setStrokeWidth(3.0);
    paint.setStrokeCap(canvasKit.StrokeCap.Round);
    paint.setStrokeJoin(canvasKit.StrokeJoin.Bevel);
    paint.setAntiAlias(true);
    paint.setColorInt(0x00FFCCAA);
    paint.setShader(_makeTestShader());
    paint.setMaskFilter(canvasKit.MaskFilter.MakeBlur(
      canvasKit.BlurStyle.Outer,
      2.0,
      true,
    ));
    paint.setColorFilter(canvasKit.ColorFilter.MakeLinearToSRGBGamma());
    paint.setStrokeMiter(1.4);
    paint.setImageFilter(canvasKit.ImageFilter.MakeBlur(
      1,
      2,
      canvasKit.TileMode.Repeat,
      null,
    ));
  });
}

void _maskFilterTests() {
  test('MaskFilter.MakeBlur', () {
    expect(
        canvasKit.MaskFilter.MakeBlur(
          canvasKit.BlurStyle.Outer,
          5.0,
          false,
        ),
        isNotNull);
  });
  test('MaskFilter.MakeBlur with 0 sigma returns null', () {
    expect(
        canvasKit.MaskFilter.MakeBlur(canvasKit.BlurStyle.Normal, 0.0, false),
        isNull);
  });
  test('MaskFilter.MakeBlur with NaN sigma returns null', () {
    expect(
        canvasKit.MaskFilter.MakeBlur(
            canvasKit.BlurStyle.Normal, double.nan, false),
        isNull);
  });
}

void _colorFilterTests() {
  test('MakeBlend', () {
    expect(
      canvasKit.ColorFilter.MakeBlend(
        Float32List.fromList(<double>[0, 0, 0, 1]),
        canvasKit.BlendMode.SrcATop,
      ),
      isNotNull,
    );
  });

  test('MakeMatrix', () {
    expect(
      canvasKit.ColorFilter.MakeMatrix(
        Float32List(20),
      ),
      isNotNull,
    );
  });

  test('MakeSRGBToLinearGamma', () {
    expect(
      canvasKit.ColorFilter.MakeSRGBToLinearGamma(),
      isNotNull,
    );
  });

  test('MakeLinearToSRGBGamma', () {
    expect(
      canvasKit.ColorFilter.MakeLinearToSRGBGamma(),
      isNotNull,
    );
  });
}

void _imageFilterTests() {
  test('MakeBlur', () {
    expect(
      canvasKit.ImageFilter.MakeBlur(1, 2, canvasKit.TileMode.Repeat, null),
      isNotNull,
    );
  });

  test('toSkFilterOptions', () {
    for (final ui.FilterQuality filterQuality in ui.FilterQuality.values) {
      expect(toSkFilterOptions(filterQuality), isNotNull);
    }
  });

  test('MakeMatrixTransform', () {
    expect(
      canvasKit.ImageFilter.MakeMatrixTransform(
        toSkMatrixFromFloat32(Matrix4.identity().storage),
        toSkFilterOptions(ui.FilterQuality.medium),
        null,
      ),
      isNotNull,
    );
  });

  test('MakeColorFilter', () {
    expect(
      canvasKit.ImageFilter.MakeColorFilter(
        canvasKit.ColorFilter.MakeLinearToSRGBGamma(),
        null,
      ),
      isNotNull,
    );
  });

  test('MakeCompose', () {
    expect(
      canvasKit.ImageFilter.MakeCompose(
        canvasKit.ImageFilter.MakeBlur(1, 2, canvasKit.TileMode.Repeat, null),
        canvasKit.ImageFilter.MakeBlur(1, 2, canvasKit.TileMode.Repeat, null),
      ),
      isNotNull,
    );
  });
}

void _mallocTests() {
  test('$SkFloat32List', () {
    final List<SkFloat32List> lists = <SkFloat32List>[];

    for (int size = 0; size < 1000; size++) {
      final SkFloat32List skList = mallocFloat32List(4);
      expect(skList, isNotNull);
      expect(skList.toTypedArray(), hasLength(4));
      lists.add(skList);
    }

    for (final SkFloat32List skList in lists) {
      // toTypedArray() still works.
      expect(() => skList.toTypedArray(), returnsNormally);
      free(skList);
      // toTypedArray() throws after free.
      expect(() => skList.toTypedArray(), throwsA(isA<Error>()));
    }
  });
  test('$SkUint32List', () {
    final List<SkUint32List> lists = <SkUint32List>[];

    for (int size = 0; size < 1000; size++) {
      final SkUint32List skList = mallocUint32List(4);
      expect(skList, isNotNull);
      expect(skList.toTypedArray(), hasLength(4));
      lists.add(skList);
    }

    for (final SkUint32List skList in lists) {
      // toTypedArray() still works.
      expect(() => skList.toTypedArray(), returnsNormally);
      free(skList);
      // toTypedArray() throws after free.
      expect(() => skList.toTypedArray(), throwsA(isA<Error>()));
    }
  });
}

void _sharedColorTests() {
  test('toSharedSkColor1', () {
    expect(
      toSharedSkColor1(const ui.Color(0xAABBCCDD)),
      Float32List(4)
        ..[0] = 0xBB / 255.0
        ..[1] = 0xCC / 255.0
        ..[2] = 0xDD / 255.0
        ..[3] = 0xAA / 255.0,
    );
  });
  test('toSharedSkColor2', () {
    expect(
      toSharedSkColor2(const ui.Color(0xAABBCCDD)),
      Float32List(4)
        ..[0] = 0xBB / 255.0
        ..[1] = 0xCC / 255.0
        ..[2] = 0xDD / 255.0
        ..[3] = 0xAA / 255.0,
    );
  });
  test('toSharedSkColor3', () {
    expect(
      toSharedSkColor3(const ui.Color(0xAABBCCDD)),
      Float32List(4)
        ..[0] = 0xBB / 255.0
        ..[1] = 0xCC / 255.0
        ..[2] = 0xDD / 255.0
        ..[3] = 0xAA / 255.0,
    );
  });
}

void _toSkPointTests() {
  test('toSkPoint', () {
    expect(
      toSkPoint(const ui.Offset(4, 5)),
      Float32List(2)
        ..[0] = 4.0
        ..[1] = 5.0,
    );
  });
}

void _toSkColorStopsTests() {
  test('toSkColorStops default', () {
    expect(
      toSkColorStops(null),
      Float32List(2)
        ..[0] = 0
        ..[1] = 1,
    );
  });

  test('toSkColorStops custom', () {
    expect(
      toSkColorStops(<double>[1, 2, 3, 4]),
      Float32List(4)
        ..[0] = 1
        ..[1] = 2
        ..[2] = 3
        ..[3] = 4,
    );
  });
}

void _toSkMatrixFromFloat32Tests() {
  test('toSkMatrixFromFloat32', () {
    final Matrix4 matrix = Matrix4.identity()
      ..translate(1, 2, 3)
      ..rotateZ(4);
    expect(
        toSkMatrixFromFloat32(matrix.storage),
        Float32List.fromList(<double>[
          -0.6536436080932617,
          0.756802499294281,
          1,
          -0.756802499294281,
          -0.6536436080932617,
          2,
          -0.0,
          0,
          1,
        ]));
  });
}

void _toSkM44FromFloat32Tests() {
  test('toSkM44FromFloat32', () {
    final Matrix4 matrix = Matrix4.identity()
      ..translate(1, 2, 3)
      ..rotateZ(4);
    expect(
        toSkM44FromFloat32(matrix.storage),
        Float32List.fromList(<double>[
          -0.6536436080932617,
          0.756802499294281,
          0,
          1,
          -0.756802499294281,
          -0.6536436080932617,
          0,
          2,
          0,
          0,
          1,
          3,
          0,
          0,
          0,
          1,
        ]));
  });
}

typedef CanvasCallback = void Function(ui.Canvas canvas);

Future<ui.Image> toImage(CanvasCallback callback, int width, int height) {
  final ui.PictureRecorder recorder = ui.PictureRecorder();
  final ui.Canvas canvas = ui.Canvas(
      recorder, ui.Rect.fromLTRB(0, 0, width.toDouble(), height.toDouble()));
  callback(canvas);
  final ui.Picture picture = recorder.endRecording();
  return picture.toImage(width, height);
}

/// @returns true When the images are reasonably similar.
/// @todo Make the search actually fuzzy to a certain degree.
Future<bool> fuzzyCompareImages(ui.Image golden, ui.Image img) async {
  if (golden.width != img.width || golden.height != img.height) {
    return false;
  }
  int getPixel(ByteData data, int x, int y) =>
      data.getUint32((x + y * golden.width) * 4);
  final ByteData goldenData = (await golden.toByteData())!;
  final ByteData imgData = (await img.toByteData())!;
  for (int y = 0; y < golden.height; y++) {
    for (int x = 0; x < golden.width; x++) {
      if (getPixel(goldenData, x, y) != getPixel(imgData, x, y)) {
        return false;
      }
    }
  }
  return true;
}

void _matrix4x4CompositionTests() {
  test('compose4x4MatrixInCanvas', () async {
    const double rotateAroundX = pi / 6; // 30 degrees
    const double rotateAroundY = pi / 9; // 20 degrees
    const int width = 150;
    const int height = 150;
    const ui.Color black = ui.Color.fromARGB(255, 0, 0, 0);
    const ui.Color green = ui.Color.fromARGB(255, 0, 255, 0);
    void paint(ui.Canvas canvas, CanvasCallback rotate) {
      canvas.translate(width * 0.5, height * 0.5);
      rotate(canvas);
      const double width3 = width / 3.0;
      const double width5 = width / 5.0;
      const double width10 = width / 10.0;
      canvas.drawRect(const ui.Rect.fromLTRB(-width3, -width3, width3, width3),
          ui.Paint()..color = green);
      canvas.drawRect(
          const ui.Rect.fromLTRB(-width5, -width5, -width10, width5),
          ui.Paint()..color = black);
      canvas.drawRect(
          const ui.Rect.fromLTRB(-width5, -width5, width5, -width10),
          ui.Paint()..color = black);
    }

    final ui.Image incrementalMatrixImage = await toImage((ui.Canvas canvas) {
      paint(canvas, (ui.Canvas canvas) {
        final Matrix4 matrix = Matrix4.identity();
        matrix.setEntry(3, 2, 0.001);
        canvas.transform(matrix.toFloat64());
        matrix.setRotationX(rotateAroundX);
        canvas.transform(matrix.toFloat64());
        matrix.setRotationY(rotateAroundY);
        canvas.transform(matrix.toFloat64());
      });
    }, width, height);
    final ui.Image combinedMatrixImage = await toImage((ui.Canvas canvas) {
      paint(canvas, (ui.Canvas canvas) {
        final Matrix4 matrix = Matrix4.identity();
        matrix.setEntry(3, 2, 0.001);
        matrix.rotate(kUnitX, rotateAroundX);
        matrix.rotate(kUnitY, rotateAroundY);
        canvas.transform(matrix.toFloat64());
      });
    }, width, height);

    final bool areEqual =
        await fuzzyCompareImages(incrementalMatrixImage, combinedMatrixImage);
    expect(areEqual, true);
  });
}

void _toSkRectTests() {
  test('toSkRect', () {
    expect(toSkRect(const ui.Rect.fromLTRB(1, 2, 3, 4)), <double>[1, 2, 3, 4]);
  });

  test('fromSkRect', () {
    expect(fromSkRect(Float32List.fromList(<double>[1, 2, 3, 4])),
        const ui.Rect.fromLTRB(1, 2, 3, 4));
  });

  test('toSkRRect', () {
    expect(
      toSkRRect(ui.RRect.fromLTRBAndCorners(
        1,
        2,
        3,
        4,
        topLeft: const ui.Radius.elliptical(5, 6),
        topRight: const ui.Radius.elliptical(7, 8),
        bottomRight: const ui.Radius.elliptical(9, 10),
        bottomLeft: const ui.Radius.elliptical(11, 12),
      )),
      <double>[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
    );
  });
}

SkPath _testClosedSkPath() {
  return SkPath()
    ..moveTo(10, 10)
    ..lineTo(20, 10)
    ..lineTo(20, 20)
    ..lineTo(10, 20)
    ..close();
}

void _pathTests() {
  late SkPath path;

  setUp(() {
    path = SkPath();
  });

  test('setFillType', () {
    path.setFillType(canvasKit.FillType.Winding);
  });

  test('addArc', () {
    path.addArc(
      toSkRect(const ui.Rect.fromLTRB(10, 20, 30, 40)),
      1,
      5,
    );
  });

  test('addOval', () {
    path.addOval(
      toSkRect(const ui.Rect.fromLTRB(10, 20, 30, 40)),
      false,
      1,
    );
  });

  test('addPath', () {
    path.addPath(_testClosedSkPath(), 1, 0, 0, 0, 1, 0, 0, 0, 0, false);
  });

  test('addPoly', () {
    final SkFloat32List encodedPoints = toMallocedSkPoints(const <ui.Offset>[
      ui.Offset.zero,
      ui.Offset(10, 10),
    ]);
    path.addPoly(encodedPoints.toTypedArray(), true);
    free(encodedPoints);
  });

  test('addRRect', () {
    final ui.RRect rrect = ui.RRect.fromRectAndRadius(
      const ui.Rect.fromLTRB(10, 10, 20, 20),
      const ui.Radius.circular(3),
    );
    path.addRRect(
      toSkRRect(rrect),
      false,
    );
  });

  test('addRect', () {
    path.addRect(toSkRect(const ui.Rect.fromLTRB(1, 2, 3, 4)));
  });

  test('arcTo', () {
    path.arcToOval(
      toSkRect(const ui.Rect.fromLTRB(1, 2, 3, 4)),
      5,
      40,
      false,
    );
  });

  test('overloaded arcTo (used for arcToPoint)', () {
    path.arcToRotated(
      1,
      2,
      3,
      false,
      true,
      4,
      5,
    );
  });

  test('close', () {
    _testClosedSkPath();
  });

  test('conicTo', () {
    path.conicTo(1, 2, 3, 4, 5);
  });

  test('contains', () {
    final SkPath testPath = _testClosedSkPath();
    expect(testPath.contains(15, 15), isTrue);
    expect(testPath.contains(100, 100), isFalse);
  });

  test('cubicTo', () {
    path.cubicTo(1, 2, 3, 4, 5, 6);
  });

  test('getBounds', () {
    final SkPath testPath = _testClosedSkPath();
    final ui.Rect bounds = fromSkRect(testPath.getBounds());
    expect(bounds, const ui.Rect.fromLTRB(10, 10, 20, 20));
  });

  test('lineTo', () {
    path.lineTo(10, 10);
  });

  test('moveTo', () {
    path.moveTo(10, 10);
  });

  test('quadTo', () {
    path.quadTo(10, 10, 20, 20);
  });

  test('rArcTo', () {
    path.rArcTo(
      10,
      20,
      30,
      false,
      true,
      40,
      50,
    );
  });

  test('rConicTo', () {
    path.rConicTo(1, 2, 3, 4, 5);
  });

  test('rCubicTo', () {
    path.rCubicTo(1, 2, 3, 4, 5, 6);
  });

  test('rLineTo', () {
    path.rLineTo(10, 10);
  });

  test('rMoveTo', () {
    path.rMoveTo(10, 10);
  });

  test('rQuadTo', () {
    path.rQuadTo(10, 10, 20, 20);
  });

  test('reset', () {
    final SkPath testPath = _testClosedSkPath();
    expect(fromSkRect(testPath.getBounds()),
        const ui.Rect.fromLTRB(10, 10, 20, 20));
    testPath.reset();
    expect(fromSkRect(testPath.getBounds()), ui.Rect.zero);
  });

  test('toSVGString', () {
    expect(
        _testClosedSkPath().toSVGString(), 'M10 10L20 10L20 20L10 20L10 10Z');
  });

  test('isEmpty', () {
    expect(SkPath().isEmpty(), isTrue);
    expect(_testClosedSkPath().isEmpty(), isFalse);
  });

  test('copy', () {
    final SkPath original = _testClosedSkPath();
    final SkPath copy = original.copy();
    expect(fromSkRect(original.getBounds()), fromSkRect(copy.getBounds()));
  });

  test('transform', () {
    path = _testClosedSkPath();
    path.transform(2, 0, 10, 0, 2, 10, 0, 0, 0);
    final ui.Rect transformedBounds = fromSkRect(path.getBounds());
    expect(transformedBounds, const ui.Rect.fromLTRB(30, 30, 50, 50));
  });

  test('SkContourMeasureIter/SkContourMeasure', () {
    final SkContourMeasureIter iter =
        SkContourMeasureIter(_testClosedSkPath(), false, 1.0);
    final SkContourMeasure measure1 = iter.next()!;
    expect(measure1.length(), 40);
    expect(measure1.getPosTan(5), Float32List.fromList(<double>[15, 10, 1, 0]));
    expect(
        measure1.getPosTan(15), Float32List.fromList(<double>[20, 15, 0, 1]));
    expect(measure1.isClosed(), isTrue);

    // Starting with a box path:
    //
    //    10         20
    // 10 +-----------+
    //    |           |
    //    |           |
    //    |           |
    //    |           |
    //    |           |
    // 20 +-----------+
    //
    // Cut out the top-right quadrant:
    //
    //    10    15   20
    // 10 +-----+=====+
    //    |     ║+++++║
    //    |     ║+++++║
    //    |     +=====+ 15
    //    |           |
    //    |           |
    // 20 +-----------+
    final SkPath segment = measure1.getSegment(5, 15, true);
    expect(fromSkRect(segment.getBounds()),
        const ui.Rect.fromLTRB(15, 10, 20, 15));

    final SkContourMeasure? measure2 = iter.next();
    expect(measure2, isNull);
  });

  test('SkPath.toCmds and CanvasKit.Path.MakeFromCmds', () {
    const ui.Rect rect = ui.Rect.fromLTRB(0, 0, 10, 10);
    final SkPath path = SkPath();
    path.addRect(toSkRect(rect));
    expect(path.toCmds(), <num>[
      0, 0, 0, // moveTo
      1, 10, 0, // lineTo
      1, 10, 10, // lineTo
      1, 0, 10, // lineTo
      5, // close
    ]);

    final SkPath copy = canvasKit.Path.MakeFromCmds(path.toCmds());
    expect(fromSkRect(copy.getBounds()), rect);
  });
}

SkVertices _testVertices() {
  return canvasKit.MakeVertices(
    canvasKit.VertexMode.Triangles,
    Float32List.fromList(<double>[0, 0, 10, 10, 0, 20]),
    Float32List.fromList(<double>[0, 0, 10, 10, 0, 20]),
    Uint32List.fromList(<int>[0xffff0000, 0xff00ff00, 0xff0000ff]),
    Uint16List.fromList(<int>[0, 1, 2]),
  );
}

void _skVerticesTests() {
  test('SkVertices', () {
    expect(_testVertices(), isNotNull);
  });
}

void _pictureTests() {
  late SkPicture picture;

  setUp(() {
    final SkPictureRecorder recorder = SkPictureRecorder();
    final SkCanvas canvas = recorder.beginRecording(toSkRect(ui.Rect.largest));
    canvas.drawRect(toSkRect(const ui.Rect.fromLTRB(20, 30, 40, 50)),
        SkPaint()..setColorInt(0xffff00ff));
    picture = recorder.finishRecordingAsPicture();
  });
  test('cullRect', () {
    expect(
        fromSkRect(picture.cullRect()), const ui.Rect.fromLTRB(20, 30, 40, 50));
  });

  test('approximateBytesUsed', () {
    expect(picture.approximateBytesUsed() > 0, isTrue);
  });
}

void _canvasTests() {
  late SkPictureRecorder recorder;
  late SkCanvas canvas;

  setUp(() {
    recorder = SkPictureRecorder();
    canvas = recorder
        .beginRecording(toSkRect(const ui.Rect.fromLTRB(0, 0, 100, 100)));
  });

  tearDown(() {
    expect(recorder.finishRecordingAsPicture(), isNotNull);
  });

  test('save/getSaveCount/restore/restoreToCount', () {
    expect(canvas.save(), 1);
    expect(canvas.save(), 2);
    expect(canvas.save(), 3);
    expect(canvas.save(), 4);
    expect(canvas.getSaveCount(), 5);
    canvas.restoreToCount(2);
    expect(canvas.getSaveCount(), 2);
    canvas.restore();
    expect(canvas.getSaveCount(), 1);
  });

  test('saveLayer', () {
    canvas.saveLayer(
      SkPaint(),
      toSkRect(const ui.Rect.fromLTRB(0, 0, 100, 100)),
      null,
      null,
    );
  });

  test('saveLayer without bounds', () {
    canvas.saveLayer(SkPaint(), null, null, null);
  });

  test('saveLayer with filter', () {
    canvas.saveLayer(
      SkPaint(),
      toSkRect(const ui.Rect.fromLTRB(0, 0, 100, 100)),
      canvasKit.ImageFilter.MakeBlur(1, 2, canvasKit.TileMode.Repeat, null),
      0,
    );
  });

  test('clear', () {
    canvas.clear(Float32List.fromList(<double>[0, 0, 0, 0]));
  });

  test('clipPath', () {
    canvas.clipPath(
      SkPath()
        ..moveTo(10.9, 10.9)
        ..lineTo(19.1, 10.9)
        ..lineTo(19.1, 19.1)
        ..lineTo(10.9, 19.1),
      canvasKit.ClipOp.Intersect,
      true,
    );
    expect(canvas.getDeviceClipBounds(), <int>[10, 10, 20, 20]);
  });

  test('clipRRect', () {
    canvas.clipRRect(
      Float32List.fromList(<double>[0.9, 0.9, 99.1, 99.1, 1, 2, 3, 4, 5, 6, 7, 8]),
      canvasKit.ClipOp.Intersect,
      true,
    );
    expect(canvas.getDeviceClipBounds(), <int>[0, 0, 100, 100]);
  });

  test('clipRect', () {
    canvas.clipRect(
      Float32List.fromList(<double>[0.9, 0.9, 99.1, 99.1]),
      canvasKit.ClipOp.Intersect,
      true,
    );
    expect(canvas.getDeviceClipBounds(), <int>[0, 0, 100, 100]);
  });

  test('drawArc', () {
    canvas.drawArc(
      Float32List.fromList(<double>[0, 0, 100, 50]),
      0,
      100,
      true,
      SkPaint(),
    );
  });

  test('drawAtlas', () {
    final SkAnimatedImage image =
        canvasKit.MakeAnimatedImageFromEncoded(kTransparentImage)!;
    canvas.drawAtlas(
      image.makeImageAtCurrentFrame(),
      Float32List.fromList(<double>[0, 0, 1, 1]),
      Float32List.fromList(<double>[1, 0, 2, 3]),
      SkPaint(),
      canvasKit.BlendMode.SrcOver,
      Uint32List.fromList(<int>[0xff000000, 0xffffffff]),
    );
  });

  test('drawCircle', () {
    canvas.drawCircle(1, 2, 3, SkPaint());
  });

  test('drawColorInt', () {
    canvas.drawColorInt(0xFFFFFFFF, canvasKit.BlendMode.SoftLight);
  });

  test('drawDRRect', () {
    canvas.drawDRRect(
      Float32List.fromList(<double>[0, 0, 100, 100, 1, 2, 3, 4, 5, 6, 7, 8]),
      Float32List.fromList(<double>[20, 20, 80, 80, 1, 2, 3, 4, 5, 6, 7, 8]),
      SkPaint(),
    );
  });

  test('drawImageOptions', () {
    final SkAnimatedImage image =
        canvasKit.MakeAnimatedImageFromEncoded(kTransparentImage)!;
    canvas.drawImageOptions(
      image.makeImageAtCurrentFrame(),
      10,
      20,
      canvasKit.FilterMode.Linear,
      canvasKit.MipmapMode.None,
      SkPaint(),
    );
  });

  test('drawImageCubic', () {
    final SkAnimatedImage image =
        canvasKit.MakeAnimatedImageFromEncoded(kTransparentImage)!;
    canvas.drawImageCubic(
      image.makeImageAtCurrentFrame(),
      10,
      20,
      0.3,
      0.3,
      SkPaint(),
    );
  });

  test('drawImageRectOptions', () {
    final SkAnimatedImage image =
        canvasKit.MakeAnimatedImageFromEncoded(kTransparentImage)!;
    canvas.drawImageRectOptions(
      image.makeImageAtCurrentFrame(),
      Float32List.fromList(<double>[0, 0, 1, 1]),
      Float32List.fromList(<double>[0, 0, 1, 1]),
      canvasKit.FilterMode.Linear,
      canvasKit.MipmapMode.None,
      SkPaint(),
    );
  });

  test('drawImageRectCubic', () {
    final SkAnimatedImage image =
        canvasKit.MakeAnimatedImageFromEncoded(kTransparentImage)!;
    canvas.drawImageRectCubic(
      image.makeImageAtCurrentFrame(),
      Float32List.fromList(<double>[0, 0, 1, 1]),
      Float32List.fromList(<double>[0, 0, 1, 1]),
      0.3,
      0.3,
      SkPaint(),
    );
  });

  test('drawImageNine', () {
    final SkAnimatedImage image =
        canvasKit.MakeAnimatedImageFromEncoded(kTransparentImage)!;
    canvas.drawImageNine(
      image.makeImageAtCurrentFrame(),
      Float32List.fromList(<double>[0, 0, 1, 1]),
      Float32List.fromList(<double>[0, 0, 1, 1]),
      canvasKit.FilterMode.Linear,
      SkPaint(),
    );
  });

  test('drawLine', () {
    canvas.drawLine(0, 1, 2, 3, SkPaint());
  });

  test('drawOval', () {
    canvas.drawOval(Float32List.fromList(<double>[0, 0, 1, 1]), SkPaint());
  });

  test('drawPaint', () {
    canvas.drawPaint(SkPaint());
  });

  test('drawPath', () {
    canvas.drawPath(
      _testClosedSkPath(),
      SkPaint(),
    );
  });

  test('drawPoints', () {
    canvas.drawPoints(
      canvasKit.PointMode.Lines,
      Float32List.fromList(<double>[0, 0, 10, 10, 0, 10]),
      SkPaint(),
    );
  });

  test('drawRRect', () {
    canvas.drawRRect(
      Float32List.fromList(<double>[0, 0, 100, 100, 1, 2, 3, 4, 5, 6, 7, 8]),
      SkPaint(),
    );
  });

  test('drawRect', () {
    canvas.drawRect(
      Float32List.fromList(<double>[0, 0, 100, 100]),
      SkPaint(),
    );
  });

  test('drawShadow', () {
    for (final int flags in const <int>[0x01, 0x00]) {
      const double devicePixelRatio = 2.0;
      const double elevation = 4.0;
      const double ambientAlpha = 0.039;
      const double spotAlpha = 0.25;

      final SkPath path = _testClosedSkPath();
      final ui.Rect bounds = fromSkRect(path.getBounds());
      final double shadowX = (bounds.left + bounds.right) / 2.0;
      final double shadowY = bounds.top - 600.0;

      const ui.Color color = ui.Color(0xAABBCCDD);
      final ui.Color inAmbient =
          color.withAlpha((color.alpha * ambientAlpha).round());
      final ui.Color inSpot =
          color.withAlpha((color.alpha * spotAlpha).round());

      final SkTonalColors inTonalColors = SkTonalColors(
        ambient: makeFreshSkColor(inAmbient),
        spot: makeFreshSkColor(inSpot),
      );

      final SkTonalColors tonalColors =
          canvasKit.computeTonalColors(inTonalColors);

      canvas.drawShadow(
        path,
        Float32List(3)..[2] = devicePixelRatio * elevation,
        Float32List(3)
          ..[0] = shadowX
          ..[1] = shadowY
          ..[2] = devicePixelRatio * kLightHeight,
        devicePixelRatio * kLightRadius,
        tonalColors.ambient,
        tonalColors.spot,
        flags.toDouble(),
      );
    }
  });

  test('drawVertices', () {
    canvas.drawVertices(
      _testVertices(),
      canvasKit.BlendMode.SrcOver,
      SkPaint(),
    );
  });

  test('rotate', () {
    canvas.rotate(90, 10, 20);
    expect(canvas.getLocalToDevice(), <double>[
      0, -1, 0, 30, // tx = 10 - (-20) == 30
      1, 0, 0, 10,  // ty = 20 - 10 == 10
      0, 0, 1, 0,
      0, 0, 0, 1,
    ]);
  });

  test('scale', () {
    canvas.scale(2, 3);
    expect(canvas.getLocalToDevice(), <double>[
      2, 0, 0, 0,
      0, 3, 0, 0,
      0, 0, 1, 0,
      0, 0, 0, 1,
    ]);
  });

  test('skew', () {
    canvas.skew(4, 5);
    expect(canvas.getLocalToDevice(), <double>[
      1, 4, 0, 0,
      5, 1, 0, 0,
      0, 0, 1, 0,
      0, 0, 0, 1,
    ]);
  });

  test('concat', () {
    canvas.concat(toSkM44FromFloat32(Matrix4.identity().storage));
    expect(canvas.getLocalToDevice(), <double>[
      1, 0, 0, 0,
      0, 1, 0, 0,
      0, 0, 1, 0,
      0, 0, 0, 1,
    ]);
    canvas.concat(Float32List.fromList(<double>[
      11, 12, 13, 14,
      21, 22, 23, 24,
      31, 32, 33, 34,
      41, 42, 43, 44,
    ]));
    expect(canvas.getLocalToDevice(), <double>[
      11, 12, 13, 14,
      21, 22, 23, 24,
      31, 32, 33, 34,
      41, 42, 43, 44,
    ]);
  });

  test('translate', () {
    canvas.translate(4, 5);
    expect(canvas.getLocalToDevice(), <double>[
      1, 0, 0, 4,
      0, 1, 0, 5,
      0, 0, 1, 0,
      0, 0, 0, 1,
    ]);
  });

  test('drawPicture', () {
    final SkPictureRecorder otherRecorder = SkPictureRecorder();
    final SkCanvas otherCanvas = otherRecorder
        .beginRecording(Float32List.fromList(<double>[0, 0, 100, 100]));
    otherCanvas.drawLine(0, 0, 10, 10, SkPaint());
    canvas.drawPicture(otherRecorder.finishRecordingAsPicture());
  });

  test('drawParagraph', () {
    final CkParagraphBuilder builder = CkParagraphBuilder(
      CkParagraphStyle(),
    );
    builder.addText('Hello');
    final CkParagraph paragraph = builder.build();
    paragraph.layout(const ui.ParagraphConstraints(width: 100));
    canvas.drawParagraph(
      paragraph.skiaObject,
      10,
      20,
    );
  });

  test('Paragraph converts caret position to charactor position', () {
    final CkParagraphBuilder builder = CkParagraphBuilder(
      CkParagraphStyle(),
    );
    builder.addText('Hello there');
    final CkParagraph paragraph = builder.build();
    paragraph.layout(const ui.ParagraphConstraints(width: 100));
    ui.TextRange range = paragraph.getWordBoundary(const ui.TextPosition(offset: 5, affinity: ui.TextAffinity.upstream));
    expect(range.start, 0);
    expect(range.end, 5);

    range = paragraph.getWordBoundary(const ui.TextPosition(offset: 5));
    expect(range.start, 5);
    expect(range.end, 6);
  });

  test('Paragraph dispose', () {
    final CkParagraphBuilder builder = CkParagraphBuilder(
      CkParagraphStyle(),
    );
    builder.addText('Hello');
    final CkParagraph paragraph = builder.build();

    paragraph.dispose();
    expect(paragraph.debugDisposed, true);
  });

  test('toImage.toByteData', () async {
    final SkPictureRecorder otherRecorder = SkPictureRecorder();
    final SkCanvas otherCanvas = otherRecorder
        .beginRecording(Float32List.fromList(<double>[0, 0, 1, 1]));
    otherCanvas.drawRect(
      Float32List.fromList(<double>[0, 0, 1, 1]),
      SkPaint()..setColorInt(0xAAFFFFFF),
    );
    final CkPicture picture =
        CkPicture(otherRecorder.finishRecordingAsPicture());
    final CkImage image = await picture.toImage(1, 1) as CkImage;
    final ByteData rawData =
        await image.toByteData();
    expect(rawData.lengthInBytes, greaterThan(0));
    expect(
      rawData.buffer.asUint32List(),
      <int>[0xAAAAAAAA],
    );
    final ByteData rawStraightData =
        await image.toByteData(format: ui.ImageByteFormat.rawStraightRgba);
    expect(rawStraightData.lengthInBytes, greaterThan(0));
    expect(
      rawStraightData.buffer.asUint32List(),
      <int>[0xAAFFFFFF],
    );
    final ByteData pngData =
        await image.toByteData(format: ui.ImageByteFormat.png);
    expect(pngData.lengthInBytes, greaterThan(0));
  });
}

void _textStyleTests() {
  test('SkTextDecorationStyle mapping is correct', () {
    expect(canvasKit.DecorationStyle.Solid.value,
        ui.TextDecorationStyle.solid.index);
    expect(canvasKit.DecorationStyle.Double.value,
        ui.TextDecorationStyle.double.index);
    expect(canvasKit.DecorationStyle.Dotted.value,
        ui.TextDecorationStyle.dotted.index);
    expect(canvasKit.DecorationStyle.Dashed.value,
        ui.TextDecorationStyle.dashed.index);
    expect(canvasKit.DecorationStyle.Wavy.value,
        ui.TextDecorationStyle.wavy.index);
  });

  test('ui.TextDecorationStyle converts to SkTextDecorationStyle', () {
    for (final ui.TextDecorationStyle decorationStyle
        in ui.TextDecorationStyle.values) {
      expect(toSkTextDecorationStyle(decorationStyle).value,
          decorationStyle.index);
    }
  });

  test('SkTextBaseline mapping is correct', () {
    expect(canvasKit.TextBaseline.Alphabetic.value,
        ui.TextBaseline.alphabetic.index);
    expect(canvasKit.TextBaseline.Ideographic.value,
        ui.TextBaseline.ideographic.index);
  });

  test('ui.TextBaseline converts to SkTextBaseline', () {
    for (final ui.TextBaseline textBaseline in ui.TextBaseline.values) {
      expect(toSkTextBaseline(textBaseline).value, textBaseline.index);
    }
  });

  test('SkPlaceholderAlignment mapping is correct', () {
    expect(canvasKit.PlaceholderAlignment.Baseline.value,
        ui.PlaceholderAlignment.baseline.index);
    expect(canvasKit.PlaceholderAlignment.AboveBaseline.value,
        ui.PlaceholderAlignment.aboveBaseline.index);
    expect(canvasKit.PlaceholderAlignment.BelowBaseline.value,
        ui.PlaceholderAlignment.belowBaseline.index);
    expect(canvasKit.PlaceholderAlignment.Top.value,
        ui.PlaceholderAlignment.top.index);
    expect(canvasKit.PlaceholderAlignment.Bottom.value,
        ui.PlaceholderAlignment.bottom.index);
    expect(canvasKit.PlaceholderAlignment.Middle.value,
        ui.PlaceholderAlignment.middle.index);
  });

  test('ui.PlaceholderAlignment converts to SkPlaceholderAlignment', () {
    for (final ui.PlaceholderAlignment placeholderAlignment
        in ui.PlaceholderAlignment.values) {
      expect(toSkPlaceholderAlignment(placeholderAlignment).value,
          placeholderAlignment.index);
    }
  });
}

void _paragraphTests() {
  // This test is just a kitchen sink that blasts CanvasKit with all paragraph
  // properties all at once, making sure CanvasKit doesn't choke on anything.
  // In particular, this tests that our JS bindings are correct, such as that
  // arguments are of acceptable types and passed in the correct order.
  test('kitchensink', () async {
    final SkParagraphStyleProperties props = SkParagraphStyleProperties();
    props.textAlign = canvasKit.TextAlign.Left;
    props.textDirection = canvasKit.TextDirection.RTL;
    props.heightMultiplier = 3;
    props.textHeightBehavior = canvasKit.TextHeightBehavior.All;
    props.maxLines = 4;
    props.ellipsis = '___';
    props.textStyle = SkTextStyleProperties()
      ..backgroundColor = Float32List.fromList(<double>[0.2, 0, 0, 0.5])
      ..color = Float32List.fromList(<double>[0, 1, 0, 1])
      ..foregroundColor = Float32List.fromList(<double>[1, 0, 1, 1])
      ..decoration = 0x2
      ..decorationThickness = 2.0
      ..decorationColor = Float32List.fromList(<double>[13, 14, 15, 16])
      ..decorationStyle = canvasKit.DecorationStyle.Dotted
      ..textBaseline = canvasKit.TextBaseline.Ideographic
      ..fontSize = 48
      ..letterSpacing = 5
      ..wordSpacing = 10
      ..heightMultiplier = 1.3
      ..halfLeading = true
      ..locale = 'en_CA'
      ..fontFamilies = <String>['Roboto', 'serif']
      ..fontStyle = (SkFontStyle()
        ..slant = canvasKit.FontSlant.Upright
        ..weight = canvasKit.FontWeight.Normal)
      ..shadows = <SkTextShadow>[]
      ..fontFeatures = <SkFontFeature>[
        SkFontFeature()
          ..name = 'pnum'
          ..value = 1,
        SkFontFeature()
          ..name = 'tnum'
          ..value = 1,
      ]
    ;
    props.strutStyle = SkStrutStyleProperties()
      ..fontFamilies = <String>['Roboto', 'Noto']
      ..fontStyle = (SkFontStyle()
        ..slant = canvasKit.FontSlant.Italic
        ..weight = canvasKit.FontWeight.Bold)
      ..fontSize = 72
      ..heightMultiplier = 1.5
      ..halfLeading = true
      ..leading = 0
      ..strutEnabled = true
      ..forceStrutHeight = false;

    final SkParagraphStyle paragraphStyle = canvasKit.ParagraphStyle(props);
    final SkParagraphBuilder builder = canvasKit.ParagraphBuilder.MakeFromFontCollection(
      paragraphStyle,
      CanvasKitRenderer.instance.fontCollection.skFontCollection,
    );

    builder.addText('Hello');
    builder.addPlaceholder(
      50,
      25,
      canvasKit.PlaceholderAlignment.Middle,
      canvasKit.TextBaseline.Ideographic,
      4.0,
    );
    builder.pushStyle(canvasKit.TextStyle(SkTextStyleProperties()
      ..color = Float32List.fromList(<double>[1, 0, 0, 1])
      ..fontSize = 24
      ..fontFamilies = <String>['Roboto', 'serif']
    ));
    builder.addText('World');
    builder.pop();
    builder.pushPaintStyle(
      canvasKit.TextStyle(SkTextStyleProperties()
        ..color = Float32List.fromList(<double>[1, 0, 0, 1])
        ..fontSize = 60
        ..fontFamilies = <String>['Roboto', 'serif']
      ),
      SkPaint()..setColorInt(0xFF0000FF),
      SkPaint()..setColorInt(0xFFFF0000),
    );
    builder.addText('!');
    builder.pop();
    builder.pushStyle(
        canvasKit.TextStyle(SkTextStyleProperties()..halfLeading = true));
    builder.pop();
    if (canvasKit.ParagraphBuilder.RequiresClientICU()) {
      injectClientICU(builder);
    }
    final SkParagraph paragraph = builder.build();
    paragraph.layout(500);

    final DomCanvasElement canvas = createDomCanvasElement(
      width: 400,
      height: 160,
    );
    domDocument.body!.append(canvas);

    // TODO(yjbanov): WebGL screenshot tests do not work on Firefox - https://github.com/flutter/flutter/issues/109265
    if (!isFirefox) {
      final SkSurface surface = canvasKit.MakeWebGLCanvasSurface(canvas);
      final SkCanvas skCanvas = surface.getCanvas();
      skCanvas.drawColorInt(0xFFCCCCCC, toSkBlendMode(ui.BlendMode.srcOver));
      skCanvas.drawParagraph(paragraph, 20, 20);
      skCanvas.drawRect(
        Float32List.fromList(<double>[20, 20, 20 + paragraph.getMaxIntrinsicWidth(), 20 + paragraph.getHeight()]),
        SkPaint()
          ..setStyle(toSkPaintStyle(ui.PaintingStyle.stroke))
          ..setStrokeWidth(1)
          ..setColorInt(0xFF00FF00),
      );
      surface.flush();

      await matchGoldenFile(
        'paragraph_kitchen_sink.png',
        region: const ui.Rect.fromLTRB(0, 0, 400, 160),
      );
    }

    void expectAlmost(double actual, double expected) {
      expect(actual, within<double>(distance: actual / 100, from: expected));
    }

    expectAlmost(paragraph.getAlphabeticBaseline(), 85.5);
    expect(paragraph.didExceedMaxLines(), isFalse);
    expectAlmost(paragraph.getHeight(), 108);
    expectAlmost(paragraph.getIdeographicBaseline(), 108);
    expectAlmost(paragraph.getLongestLine(), 263);
    expectAlmost(paragraph.getMaxIntrinsicWidth(), 263);
    expectAlmost(paragraph.getMinIntrinsicWidth(), 135);
    expectAlmost(paragraph.getMaxWidth(), 500);
    final SkRectWithDirection rectWithDirection =
      paragraph.getRectsForRange(
        1,
        3,
        canvasKit.RectHeightStyle.Tight,
        canvasKit.RectWidthStyle.Max).single;
    expect(
      rectWithDirection.rect,
      hasLength(4),
    );
    expect(paragraph.getRectsForPlaceholders(), hasLength(1));
    expect(paragraph.getLineMetrics(), hasLength(1));

    final SkLineMetrics lineMetrics =
        paragraph.getLineMetrics().single;
    expectAlmost(lineMetrics.ascent, 55.6);
    expectAlmost(lineMetrics.descent, 14.8);
    expect(lineMetrics.isHardBreak, isTrue);
    expectAlmost(lineMetrics.baseline, 85.5);
    expectAlmost(lineMetrics.height, 108);
    expectAlmost(lineMetrics.left, 2.5);
    expectAlmost(lineMetrics.width, 263);
    expect(lineMetrics.lineNumber, 0);

    expect(
      paragraph.getGlyphPositionAtCoordinate(5, 5).affinity,
      canvasKit.Affinity.Upstream,
    );

    // "Hello"
    for (int i = 0; i < 5; i++) {
      expect(paragraph.getWordBoundary(i.toDouble()).start, 0);
      expect(paragraph.getWordBoundary(i.toDouble()).end, 5);
    }
    // Placeholder
    expect(paragraph.getWordBoundary(5).start, 5);
    expect(paragraph.getWordBoundary(5).end, 6);
    // "World"
    for (int i = 6; i < 11; i++) {
      expect(paragraph.getWordBoundary(i.toDouble()).start, 6);
      expect(paragraph.getWordBoundary(i.toDouble()).end, 11);
    }
    // "!"
    expect(paragraph.getWordBoundary(11).start, 11);
    expect(paragraph.getWordBoundary(11).end, 12);

    paragraph.delete();
  });

  test('RectHeightStyle', () {
    final SkParagraphStyleProperties props = SkParagraphStyleProperties();
    props.heightMultiplier = 3;
    props.textAlign = canvasKit.TextAlign.Start;
    props.textDirection = canvasKit.TextDirection.LTR;
    props.textStyle = SkTextStyleProperties()
      ..fontSize = 25
      ..fontFamilies = <String>['Roboto']
      ..fontStyle = (SkFontStyle()..weight = canvasKit.FontWeight.Normal);
    props.strutStyle = SkStrutStyleProperties()
      ..strutEnabled = true
      ..forceStrutHeight = true
      ..fontSize = 25
      ..fontFamilies = <String>['Roboto']
      ..heightMultiplier = 3
      ..fontStyle = (SkFontStyle()..weight = canvasKit.FontWeight.Normal);
    final SkParagraphStyle paragraphStyle = canvasKit.ParagraphStyle(props);
    final SkParagraphBuilder builder =
        canvasKit.ParagraphBuilder.MakeFromFontCollection(
      paragraphStyle,
      CanvasKitRenderer.instance.fontCollection.skFontCollection,
    );
    builder.addText('hello');

    if (canvasKit.ParagraphBuilder.RequiresClientICU()) {
      injectClientICU(builder);
    }

    final SkParagraph paragraph = builder.build();
    paragraph.layout(500);

    final List<SkRectWithDirection> rects = paragraph.getRectsForRange(
      0,
      1,
      canvasKit.RectHeightStyle.Strut,
      canvasKit.RectWidthStyle.Tight,
    );
    expect(rects.length, 1);
    final SkRectWithDirection rect = rects.first;
    expect(rect.rect, <double>[0, 0, 13.770000457763672, 75]);
  });

  test('TextHeightBehavior', () {
    expect(
      toSkTextHeightBehavior(const ui.TextHeightBehavior()),
      canvasKit.TextHeightBehavior.All,
    );
    expect(
      toSkTextHeightBehavior(const ui.TextHeightBehavior(
        applyHeightToFirstAscent: false,
      )),
      canvasKit.TextHeightBehavior.DisableFirstAscent,
    );
    expect(
      toSkTextHeightBehavior(const ui.TextHeightBehavior(
        applyHeightToLastDescent: false,
      )),
      canvasKit.TextHeightBehavior.DisableLastDescent,
    );
    expect(
      toSkTextHeightBehavior(const ui.TextHeightBehavior(
        applyHeightToFirstAscent: false,
        applyHeightToLastDescent: false,
      )),
      canvasKit.TextHeightBehavior.DisableAll,
    );
  });

  test('MakeOnScreenGLSurface test', () {
    final DomCanvasElement canvas = createDomCanvasElement(
      width: 100,
      height: 100,
    );
    final WebGLContext gl = canvas.getGlContext(webGLVersion);
    final int sampleCount = gl.getParameter(gl.samples);
    final int stencilBits = gl.getParameter(gl.stencilBits);

    final double glContext = canvasKit.GetWebGLContext(
      canvas,
      SkWebGLContextOptions(
        antialias: 0,
        majorVersion: webGLVersion.toDouble(),
      ),
    );
    final SkGrContext grContext =  canvasKit.MakeGrContext(glContext);
    final SkSurface? skSurface = canvasKit.MakeOnScreenGLSurface(
      grContext,
      100,
      100,
      SkColorSpaceSRGB,
      sampleCount,
      stencilBits
    );

    expect(skSurface, isNotNull);
  }, skip: isFirefox); // Intended: Headless firefox has no webgl support https://github.com/flutter/flutter/issues/109265

  test('MakeRenderTarget test', () {
    final DomCanvasElement canvas = createDomCanvasElement(
      width: 100,
      height: 100,
    );

    final int glContext = canvasKit.GetWebGLContext(
      canvas,
      SkWebGLContextOptions(
        antialias: 0,
        majorVersion: webGLVersion.toDouble(),
      ),
    ).toInt();
    final SkGrContext grContext =  canvasKit.MakeGrContext(glContext.toDouble());
    final SkSurface? surface = canvasKit.MakeRenderTarget(grContext, 1, 1);

    expect(surface, isNotNull);
  }, skip: isFirefox); // Intended: Headless firefox has no webgl support https://github.com/flutter/flutter/issues/109265

  group('getCanvasKitJsFileNames', () {
    dynamic oldV8BreakIterator = v8BreakIterator;
    dynamic oldIntlSegmenter = intlSegmenter;

    setUp(() {
      oldV8BreakIterator = v8BreakIterator;
      oldIntlSegmenter = intlSegmenter;
    });
    tearDown(() {
      v8BreakIterator = oldV8BreakIterator;
      intlSegmenter = oldIntlSegmenter;
      debugResetBrowserSupportsImageDecoder();
    });

    test('in Chromium-based browsers', () {
      v8BreakIterator = Object(); // Any non-null value.
      intlSegmenter = Object(); // Any non-null value.
      browserSupportsImageDecoder = true;

      expect(getCanvasKitJsFileNames(CanvasKitVariant.full), <String>['canvaskit.js']);
      expect(getCanvasKitJsFileNames(CanvasKitVariant.chromium), <String>['chromium/canvaskit.js']);
      expect(getCanvasKitJsFileNames(CanvasKitVariant.auto), <String>[
        'chromium/canvaskit.js',
        'canvaskit.js',
      ]);
    });

    test('in older versions of Chromium-based browsers', () {
      v8BreakIterator = Object(); // Any non-null value.
      intlSegmenter = null; // Older versions of Chromium didn't have the Intl.Segmenter API.
      browserSupportsImageDecoder = true;

      expect(getCanvasKitJsFileNames(CanvasKitVariant.full), <String>['canvaskit.js']);
      expect(getCanvasKitJsFileNames(CanvasKitVariant.chromium), <String>['chromium/canvaskit.js']);
      expect(getCanvasKitJsFileNames(CanvasKitVariant.auto), <String>['canvaskit.js']);
    });

    test('in other browsers', () {
      intlSegmenter = Object(); // Any non-null value.

      v8BreakIterator = null;
      browserSupportsImageDecoder = true;
      expect(getCanvasKitJsFileNames(CanvasKitVariant.full), <String>['canvaskit.js']);
      expect(getCanvasKitJsFileNames(CanvasKitVariant.chromium), <String>['chromium/canvaskit.js']);
      expect(getCanvasKitJsFileNames(CanvasKitVariant.auto), <String>['canvaskit.js']);

      v8BreakIterator = Object();
      browserSupportsImageDecoder = false;
      // TODO(mdebbar): we don't check image codecs for now.
      // https://github.com/flutter/flutter/issues/122331
      expect(getCanvasKitJsFileNames(CanvasKitVariant.full), <String>['canvaskit.js']);
      expect(getCanvasKitJsFileNames(CanvasKitVariant.chromium), <String>['chromium/canvaskit.js']);
      expect(getCanvasKitJsFileNames(CanvasKitVariant.auto), <String>['chromium/canvaskit.js', 'canvaskit.js']);

      v8BreakIterator = null;
      browserSupportsImageDecoder = false;
      expect(getCanvasKitJsFileNames(CanvasKitVariant.full), <String>['canvaskit.js']);
      expect(getCanvasKitJsFileNames(CanvasKitVariant.chromium), <String>['chromium/canvaskit.js']);
      expect(getCanvasKitJsFileNames(CanvasKitVariant.auto), <String>['canvaskit.js']);
    });
  });

  test('respects actual location of canvaskit files', () {
    expect(
      canvasKitWasmModuleUrl('canvaskit.wasm', 'https://example.com/'),
      'https://example.com/canvaskit.wasm',
    );
    expect(
      canvasKitWasmModuleUrl('canvaskit.wasm', 'http://localhost:1234/'),
      'http://localhost:1234/canvaskit.wasm',
    );
    expect(
      canvasKitWasmModuleUrl('canvaskit.wasm', 'http://localhost:1234/foo/'),
      'http://localhost:1234/foo/canvaskit.wasm',
    );
  });

  test('SkObjectFinalizationRegistry', () {
    // There's no reliable way to test the actual functionality of
    // FinalizationRegistry because it depends on GC, which cannot be controlled,
    // So the test simply tests that a FinalizationRegistry can be constructed
    // and its `register` method can be called.
    final DomFinalizationRegistry registry = createDomFinalizationRegistry((String arg) {}.toJS);
    registry.register(Object(), Object());
  });
}


@JS('window.Intl.v8BreakIterator')
external dynamic get v8BreakIterator;

@JS('window.Intl.v8BreakIterator')
external set v8BreakIterator(dynamic x);

@JS('window.Intl.Segmenter')
external dynamic get intlSegmenter;

@JS('window.Intl.Segmenter')
external set intlSegmenter(dynamic x);
