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

@TestOn('!chrome')
import 'dart:typed_data';
import 'dart:ui' as ui show Image;

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

import '../image_data.dart';

ByteData testByteData(double scale) => ByteData(8)..setFloat64(0, scale);
double scaleOf(ByteData data) => data.getFloat64(0);

const String testManifest = '''
{
  "assets/image.png" : [
    "assets/image.png",
    "assets/1.5x/image.png",
    "assets/2.0x/image.png",
    "assets/3.0x/image.png",
    "assets/4.0x/image.png"
  ]
}
''';

class TestAssetBundle extends CachingAssetBundle {
  TestAssetBundle({ this.manifest = testManifest });

  final String manifest;

  @override
  Future<ByteData> load(String key) {
    late ByteData data;
    switch (key) {
      case 'assets/image.png':
        data = testByteData(1.0);
        break;
      case 'assets/1.0x/image.png':
        data = testByteData(10.0); // see "...with a main asset and a 1.0x asset"
        break;
      case 'assets/1.5x/image.png':
        data = testByteData(1.5);
        break;
      case 'assets/2.0x/image.png':
        data = testByteData(2.0);
        break;
      case 'assets/3.0x/image.png':
        data = testByteData(3.0);
        break;
      case 'assets/4.0x/image.png':
        data = testByteData(4.0);
        break;
    }
    return SynchronousFuture<ByteData>(data);
  }

  @override
  Future<String> loadString(String key, { bool cache = true }) {
    if (key == 'AssetManifest.json')
      return SynchronousFuture<String>(manifest);
    return SynchronousFuture<String>('');
  }

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

class FakeImageStreamCompleter extends ImageStreamCompleter {
  FakeImageStreamCompleter(Future<ImageInfo> image) {
    image.then<void>(setImage);
  }
}

class TestAssetImage extends AssetImage {
  const TestAssetImage(String name, this.images) : super(name);

  final Map<double, ui.Image> images;

  @override
  ImageStreamCompleter load(AssetBundleImageKey key, DecoderCallback decode) {
    late ImageInfo imageInfo;
    key.bundle.load(key.name).then<void>((ByteData data) {
      final ui.Image image = images[scaleOf(data)]!;
      assert(image != null, 'Expected ${scaleOf(data)} to have a key in $images');
      imageInfo = ImageInfo(image: image, scale: key.scale);
    });
    return FakeImageStreamCompleter(
      SynchronousFuture<ImageInfo>(imageInfo),
    );
  }
}

Widget buildImageAtRatio(String imageName, Key key, double ratio, bool inferSize, Map<double, ui.Image> images, [ AssetBundle? bundle ]) {
  const double windowSize = 500.0; // 500 logical pixels
  const double imageSize = 200.0; // 200 logical pixels

  return MediaQuery(
    data: MediaQueryData(
      size: const Size(windowSize, windowSize),
      devicePixelRatio: ratio,
    ),
    child: DefaultAssetBundle(
      bundle: bundle ?? TestAssetBundle(),
      child: Center(
        child: inferSize ?
          Image(
            key: key,
            excludeFromSemantics: true,
            image: TestAssetImage(imageName, images),
          ) :
          Image(
            key: key,
            excludeFromSemantics: true,
            image: TestAssetImage(imageName, images),
            height: imageSize,
            width: imageSize,
            fit: BoxFit.fill,
          ),
      ),
    ),
  );
}

Widget buildImageCacheResized(String name, Key key, int width, int height, int cacheWidth, int cacheHeight) {
  return Center(
    child: RepaintBoundary(
      child: SizedBox(
        width: 250,
        height: 250,
        child: Center(
          child: Image.memory(
            Uint8List.fromList(kTransparentImage),
            key: key,
            excludeFromSemantics: true,
            color: const Color(0xFF00FFFF),
            colorBlendMode: BlendMode.plus,
            width: width.toDouble(),
            height: height.toDouble(),
            cacheWidth: cacheWidth,
            cacheHeight: cacheHeight,
          ),
        ),
      ),
    ),
  );
}

RenderImage getRenderImage(WidgetTester tester, Key key) {
  return tester.renderObject<RenderImage>(find.byKey(key));
}

Future<void> pumpTreeToLayout(WidgetTester tester, Widget widget) {
  const Duration pumpDuration = Duration.zero;
  const EnginePhase pumpPhase = EnginePhase.layout;
  return tester.pumpWidget(widget, pumpDuration, pumpPhase);
}

void main() {
  const String image = 'assets/image.png';

  final Map<double, ui.Image> images = <double, ui.Image>{};
  setUpAll(() async {
    for (final double scale in const <double>[0.5, 1.0, 1.5, 2.0, 4.0, 10.0]) {
      final int dimension = (48 * scale).floor();
      images[scale] = await createTestImage(width: dimension, height: dimension);
    }
  });

  testWidgets('Image for device pixel ratio 1.0', (WidgetTester tester) async {
    const double ratio = 1.0;
    Key key = GlobalKey();
    await pumpTreeToLayout(tester, buildImageAtRatio(image, key, ratio, false, images));
    expect(getRenderImage(tester, key).size, const Size(200.0, 200.0));
    expect(getRenderImage(tester, key).scale, 1.0);
    key = GlobalKey();
    await pumpTreeToLayout(tester, buildImageAtRatio(image, key, ratio, true, images));
    expect(getRenderImage(tester, key).size, const Size(48.0, 48.0));
    expect(getRenderImage(tester, key).scale, 1.0);
  });

  testWidgets('Image for device pixel ratio 0.5', (WidgetTester tester) async {
    const double ratio = 0.5;
    Key key = GlobalKey();
    await pumpTreeToLayout(tester, buildImageAtRatio(image, key, ratio, false, images));
    expect(getRenderImage(tester, key).size, const Size(200.0, 200.0));
    expect(getRenderImage(tester, key).scale, 1.0);
    key = GlobalKey();
    await pumpTreeToLayout(tester, buildImageAtRatio(image, key, ratio, true, images));
    expect(getRenderImage(tester, key).size, const Size(48.0, 48.0));
    expect(getRenderImage(tester, key).scale, 1.0);
  });

  testWidgets('Image for device pixel ratio 1.5', (WidgetTester tester) async {
    const double ratio = 1.5;
    Key key = GlobalKey();
    await pumpTreeToLayout(tester, buildImageAtRatio(image, key, ratio, false, images));
    expect(getRenderImage(tester, key).size, const Size(200.0, 200.0));
    expect(getRenderImage(tester, key).scale, 1.5);
    key = GlobalKey();
    await pumpTreeToLayout(tester, buildImageAtRatio(image, key, ratio, true, images));
    expect(getRenderImage(tester, key).size, const Size(48.0, 48.0));
    expect(getRenderImage(tester, key).scale, 1.5);
  });

  // A 1.75 DPR screen is typically a low-resolution screen, such that physical
  // pixels are visible to the user. For such screens we prefer to pick the
  // higher resolution image, if available.
  testWidgets('Image for device pixel ratio 1.75', (WidgetTester tester) async {
    const double ratio = 1.75;
    Key key = GlobalKey();
    await pumpTreeToLayout(tester, buildImageAtRatio(image, key, ratio, false, images));
    expect(getRenderImage(tester, key).size, const Size(200.0, 200.0));
    expect(getRenderImage(tester, key).scale, 2.0);
    key = GlobalKey();
    await pumpTreeToLayout(tester, buildImageAtRatio(image, key, ratio, true, images));
    expect(getRenderImage(tester, key).size, const Size(48.0, 48.0));
    expect(getRenderImage(tester, key).scale, 2.0);
  });

  testWidgets('Image for device pixel ratio 2.3', (WidgetTester tester) async {
    const double ratio = 2.3;
    Key key = GlobalKey();
    await pumpTreeToLayout(tester, buildImageAtRatio(image, key, ratio, false, images));
    expect(getRenderImage(tester, key).size, const Size(200.0, 200.0));
    expect(getRenderImage(tester, key).scale, 2.0);
    key = GlobalKey();
    await pumpTreeToLayout(tester, buildImageAtRatio(image, key, ratio, true, images));
    expect(getRenderImage(tester, key).size, const Size(48.0, 48.0));
    expect(getRenderImage(tester, key).scale, 2.0);
  });

  testWidgets('Image for device pixel ratio 3.7', (WidgetTester tester) async {
    const double ratio = 3.7;
    Key key = GlobalKey();
    await pumpTreeToLayout(tester, buildImageAtRatio(image, key, ratio, false, images));
    expect(getRenderImage(tester, key).size, const Size(200.0, 200.0));
    expect(getRenderImage(tester, key).scale, 4.0);
    key = GlobalKey();
    await pumpTreeToLayout(tester, buildImageAtRatio(image, key, ratio, true, images));
    expect(getRenderImage(tester, key).size, const Size(48.0, 48.0));
    expect(getRenderImage(tester, key).scale, 4.0);
  });

  testWidgets('Image for device pixel ratio 5.1', (WidgetTester tester) async {
    const double ratio = 5.1;
    Key key = GlobalKey();
    await pumpTreeToLayout(tester, buildImageAtRatio(image, key, ratio, false, images));
    expect(getRenderImage(tester, key).size, const Size(200.0, 200.0));
    expect(getRenderImage(tester, key).scale, 4.0);
    key = GlobalKey();
    await pumpTreeToLayout(tester, buildImageAtRatio(image, key, ratio, true, images));
    expect(getRenderImage(tester, key).size, const Size(48.0, 48.0));
    expect(getRenderImage(tester, key).scale, 4.0);
  });

  testWidgets('Image for device pixel ratio 1.0, with no main asset', (WidgetTester tester) async {
    const String manifest = '''
    {
      "assets/image.png" : [
        "assets/1.5x/image.png",
        "assets/2.0x/image.png",
        "assets/3.0x/image.png",
        "assets/4.0x/image.png"
      ]
    }
    ''';
    final AssetBundle bundle = TestAssetBundle(manifest: manifest);

    const double ratio = 1.0;
    Key key = GlobalKey();
    await pumpTreeToLayout(tester, buildImageAtRatio(image, key, ratio, false, images, bundle));
    expect(getRenderImage(tester, key).size, const Size(200.0, 200.0));
    expect(getRenderImage(tester, key).scale, 1.5);
    key = GlobalKey();
    await pumpTreeToLayout(tester, buildImageAtRatio(image, key, ratio, true, images, bundle));
    expect(getRenderImage(tester, key).size, const Size(48.0, 48.0));
    expect(getRenderImage(tester, key).scale, 1.5);
  });

  testWidgets('Image for device pixel ratio 1.0, with a main asset and a 1.0x asset', (WidgetTester tester) async {
    // If both a main asset and a 1.0x asset are specified, then prefer
    // the 1.0x asset.

    const String manifest = '''
    {
      "assets/image.png" : [
        "assets/image.png",
        "assets/1.0x/image.png",
        "assets/1.5x/image.png",
        "assets/2.0x/image.png",
        "assets/3.0x/image.png",
        "assets/4.0x/image.png"
      ]
    }
    ''';
    final AssetBundle bundle = TestAssetBundle(manifest: manifest);

    const double ratio = 1.0;
    Key key = GlobalKey();
    await pumpTreeToLayout(tester, buildImageAtRatio(image, key, ratio, false, images, bundle));
    expect(getRenderImage(tester, key).size, const Size(200.0, 200.0));
    // Verify we got the 10x scaled image, since the test ByteData said it should be 10x.
    expect(getRenderImage(tester, key).image!.height, 480);
    key = GlobalKey();
    await pumpTreeToLayout(tester, buildImageAtRatio(image, key, ratio, true, images, bundle));
    expect(getRenderImage(tester, key).size, const Size(480.0, 480.0));
    // Verify we got the 10x scaled image, since the test ByteData said it should be 10x.
    expect(getRenderImage(tester, key).image!.height, 480);
  });

  testWidgets('Image cache resize upscale display 5', (WidgetTester tester) async {
    final Key key = GlobalKey();
    await pumpTreeToLayout(tester, buildImageCacheResized(image, key, 5, 5, 20, 20));
    expect(getRenderImage(tester, key).size, const Size(5.0, 5.0));
  });

  testWidgets('Image cache resize upscale display 50', (WidgetTester tester) async {
    final Key key = GlobalKey();
    await pumpTreeToLayout(tester, buildImageCacheResized(image, key, 50, 50, 20, 20));
    expect(getRenderImage(tester, key).size, const Size(50.0, 50.0));
  });

  testWidgets('Image cache resize downscale display 5', (WidgetTester tester) async {
    final Key key = GlobalKey();
    await pumpTreeToLayout(tester, buildImageCacheResized(image, key, 5, 5, 1, 1));
    expect(getRenderImage(tester, key).size, const Size(5.0, 5.0));
  });

  // For low-resolution screens we prefer higher-resolution images due to
  // visible physical pixel size (see the test for 1.75 DPR above). However,
  // if higher resolution assets are not available we will pick the best
  // available.
  testWidgets('Low-resolution assets', (WidgetTester tester) async {
    final AssetBundle bundle = TestAssetBundle(manifest: '''
      {
        "assets/image.png" : [
          "assets/image.png",
          "assets/1.5x/image.png"
        ]
      }
    ''');

    Future<void> testRatio({required double ratio, required double expectedScale}) async {
      Key key = GlobalKey();
      await pumpTreeToLayout(tester, buildImageAtRatio(image, key, ratio, false, images, bundle));
      expect(getRenderImage(tester, key).size, const Size(200.0, 200.0));
      expect(getRenderImage(tester, key).scale, expectedScale);
      key = GlobalKey();
      await pumpTreeToLayout(tester, buildImageAtRatio(image, key, ratio, true, images, bundle));
      expect(getRenderImage(tester, key).size, const Size(48.0, 48.0));
      expect(getRenderImage(tester, key).scale, expectedScale);
    }

    // Choose higher resolution image as it's the lowest available.
    await testRatio(ratio: 0.25, expectedScale: 1.0);
    await testRatio(ratio: 0.5, expectedScale: 1.0);
    await testRatio(ratio: 0.75, expectedScale: 1.0);
    await testRatio(ratio: 1.0, expectedScale: 1.0);

    // Choose higher resolution image even though a lower resolution
    // image is closer.
    await testRatio(ratio: 1.20, expectedScale: 1.5);

    // Choose higher resolution image because it's closer.
    await testRatio(ratio: 1.25, expectedScale: 1.5);
    await testRatio(ratio: 1.5, expectedScale: 1.5);

    // Choose lower resolution image because no higher resolution assets
    // are not available.
    await testRatio(ratio: 1.75, expectedScale: 1.5);
    await testRatio(ratio: 2.0, expectedScale: 1.5);
    await testRatio(ratio: 2.25, expectedScale: 1.5);
    await testRatio(ratio: 10.0, expectedScale: 1.5);
  });
}