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

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

class TestAssetBundle extends CachingAssetBundle {
  TestAssetBundle(this._assetBundleMap);

  final Map<String, List<Map<Object?, Object?>>> _assetBundleMap;

  Map<String, int> loadCallCount = <String, int>{};

  @override
  Future<ByteData> load(String key) async {
    if (key == 'AssetManifest.bin') {
      return const StandardMessageCodec().encodeMessage(_assetBundleMap)!;
    }

    if (key == 'AssetManifest.bin.json') {
      // Encode the manifest data that will be used by the app
      final ByteData data = const StandardMessageCodec().encodeMessage(_assetBundleMap)!;
      // Simulate the behavior of NetworkAssetBundle.load here, for web tests
      return ByteData.sublistView(
        utf8.encode(
          json.encode(
            base64.encode(
              // Encode only the actual bytes of the buffer, and no more...
              data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes)
            )
          )
        )
      );
    }

    loadCallCount[key] = loadCallCount[key] ?? 0 + 1;
    if (key == 'one') {
      return ByteData(1)
        ..setInt8(0, 49);
    }
    throw FlutterError('key not found');
  }

  @override
  Future<ui.ImmutableBuffer> loadBuffer(String key) async {
    final ByteData data = await load(key);
    return ui.ImmutableBuffer.fromUint8List(data.buffer.asUint8List());
  }
}

void main() {
  group('1.0 scale device tests', () {
    void buildAndTestWithOneAsset(String mainAssetPath) {
      final Map<String, List<Map<Object?, Object?>>> assetBundleMap =
        <String, List<Map<Object?, Object?>>>{};

      final AssetImage assetImage = AssetImage(
        mainAssetPath,
        bundle: TestAssetBundle(assetBundleMap),
      );
      const ImageConfiguration configuration = ImageConfiguration.empty;

      assetImage.obtainKey(configuration)
        .then(expectAsync1((AssetBundleImageKey bundleKey) {
          expect(bundleKey.name, mainAssetPath);
          expect(bundleKey.scale, 1.0);
        }));
    }

    test('When asset is main variant check scale is 1.0', () {
      buildAndTestWithOneAsset('assets/normalFolder/normalFile.png');
    });

    test('When asset path and key are the same string even though it could be took as a 3.0x variant', () async {
      buildAndTestWithOneAsset('assets/parentFolder/3.0x/normalFile.png');
    });

    test('When asset path contains variant identifier as part of parent folder name scale is 1.0', () {
      buildAndTestWithOneAsset('assets/parentFolder/__3.0x__/leafFolder/normalFile.png');
    });

    test('When asset path contains variant identifier as part of leaf folder name scale is 1.0', () {
      buildAndTestWithOneAsset('assets/parentFolder/__3.0x_leaf_folder_/normalFile.png');
    });

    test('When asset path contains variant identifier as part of parent folder name scale is 1.0', () {
      buildAndTestWithOneAsset('assets/parentFolder/__3.0x__/leafFolder/normalFile.png');
    });

    test('When asset path contains variant identifier in parent folder scale is 1.0', () {
      buildAndTestWithOneAsset('assets/parentFolder/3.0x/leafFolder/normalFile.png');
    });
  });


  group('High-res device behavior tests', () {
    test('When asset is not main variant check scale is not 1.0', () {
      const String mainAssetPath = 'assets/normalFolder/normalFile.png';
      const String variantPath = 'assets/normalFolder/3.0x/normalFile.png';

      final Map<String, List<Map<Object?, Object?>>> assetBundleMap =
        <String, List<Map<Object?, Object?>>>{};

      final Map<Object?, Object?> mainAssetVariantManifestEntry = <Object?, Object?>{};
      mainAssetVariantManifestEntry['asset'] = variantPath;
      mainAssetVariantManifestEntry['dpr'] = 3.0;
      assetBundleMap[mainAssetPath] = <Map<Object?, Object?>>[mainAssetVariantManifestEntry];
      final TestAssetBundle testAssetBundle = TestAssetBundle(assetBundleMap);

      final AssetImage assetImage = AssetImage(
        mainAssetPath,
        bundle: testAssetBundle,
      );

      assetImage.obtainKey(ImageConfiguration.empty)
        .then(expectAsync1((AssetBundleImageKey bundleKey) {
          expect(bundleKey.name, mainAssetPath);
          expect(bundleKey.scale, 1.0);
        }));

      assetImage.obtainKey(ImageConfiguration(
        bundle: testAssetBundle,
        devicePixelRatio: 3.0,
      )).then(expectAsync1((AssetBundleImageKey bundleKey) {
        expect(bundleKey.name, variantPath);
        expect(bundleKey.scale, 3.0);
      }));
    });

    test('When high-res device and high-res asset not present in bundle then return main variant', () {
      const String mainAssetPath = 'assets/normalFolder/normalFile.png';

      final Map<String, List<Map<Object?, Object?>>> assetBundleMap =
        <String, List<Map<Object?, Object?>>>{};

      assetBundleMap[mainAssetPath] = <Map<Object?, Object?>>[];

      final TestAssetBundle testAssetBundle = TestAssetBundle(assetBundleMap);

      final AssetImage assetImage = AssetImage(
        mainAssetPath,
        bundle: TestAssetBundle(assetBundleMap),
      );


      assetImage.obtainKey(ImageConfiguration.empty)
        .then(expectAsync1((AssetBundleImageKey bundleKey) {
          expect(bundleKey.name, mainAssetPath);
          expect(bundleKey.scale, 1.0);
        }));

      assetImage.obtainKey(ImageConfiguration(
        bundle: testAssetBundle,
        devicePixelRatio: 3.0,
      )).then(expectAsync1((AssetBundleImageKey bundleKey) {
        expect(bundleKey.name, mainAssetPath);
        expect(bundleKey.scale, 1.0);
      }));
    });
  });

  group('Regression - When assets available are 1.0 and 3.0 check devices with a range of scales', () {
    const String mainAssetPath = 'assets/normalFolder/normalFile.png';
    const String variantPath = 'assets/normalFolder/3.0x/normalFile.png';


    void buildBundleAndTestVariantLogic(
      double deviceRatio,
      double chosenAssetRatio,
      String expectedAssetPath,
    ) {
      const Map<String, List<Map<Object?, Object?>>> assetManifest =
          <String, List<Map<Object?, Object?>>>{
        'assets/normalFolder/normalFile.png': <Map<Object?, Object?>>[
          <Object?, Object?>{'asset': 'assets/normalFolder/normalFile.png'},
          <Object?, Object?>{
            'asset': 'assets/normalFolder/3.0x/normalFile.png',
            'dpr': 3.0
          },
        ]
      };
      final TestAssetBundle testAssetBundle = TestAssetBundle(assetManifest);

      final AssetImage assetImage = AssetImage(
        mainAssetPath,
        bundle: testAssetBundle,
      );

      // we have 1.0 and 3.0, asking for 1.5 should give
      assetImage.obtainKey(ImageConfiguration(
        bundle: testAssetBundle,
        devicePixelRatio: deviceRatio,
      )).then(expectAsync1((AssetBundleImageKey bundleKey) {
        expect(bundleKey.name, expectedAssetPath);
        expect(bundleKey.scale, chosenAssetRatio);
      }));
    }

    test('Obvious case 1.0 - we have exact asset', () {
      buildBundleAndTestVariantLogic(1.0, 1.0, mainAssetPath);
    });

    test('Obvious case 3.0 - we have exact asset', () {
      buildBundleAndTestVariantLogic(3.0, 3.0, variantPath);
    });

    test('Typical case 2.0', () {
      buildBundleAndTestVariantLogic(2.0, 1.0, mainAssetPath);
    });

    test('Borderline case 2.01', () {
      buildBundleAndTestVariantLogic(2.01, 3.0, variantPath);
    });
    test('Borderline case 2.9', () {
      buildBundleAndTestVariantLogic(2.9, 3.0, variantPath);
    });

    test('Typical case 4.0', () {
      buildBundleAndTestVariantLogic(4.0, 3.0, variantPath);
    });
  });

}