// 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:io';
import 'dart:math' as math;
import 'dart:typed_data';

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

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

void main() {

  final DecoderCallback basicDecoder = (Uint8List bytes, {int cacheWidth, int cacheHeight}) {
    return PaintingBinding.instance.instantiateImageCodec(bytes, cacheWidth: cacheWidth, cacheHeight: cacheHeight);
  };

  setUpAll(() {
    TestRenderingFlutterBinding(); // initializes the imageCache
  });

  FlutterExceptionHandler oldError;
  setUp(() {
    oldError = FlutterError.onError;
  });

  tearDown(() {
    FlutterError.onError = oldError;
    PaintingBinding.instance.imageCache.clear();
  });

  group('ImageProvider', () {
    group('Image cache', () {
      tearDown(() {
        imageCache.clear();
      });

      test('AssetImageProvider - evicts on failure to load', () async {
        final Completer<FlutterError> error = Completer<FlutterError>();
        FlutterError.onError = (FlutterErrorDetails details) {
          error.complete(details.exception as FlutterError);
        };

        const ImageProvider provider = ExactAssetImage('does-not-exist');
        final Object key = await provider.obtainKey(ImageConfiguration.empty);
        expect(imageCache.containsKey(provider), false);

        provider.resolve(ImageConfiguration.empty);

        expect(imageCache.containsKey(key), true);

        await error.future;

        expect(imageCache.containsKey(provider), false);
      }, skip: isBrowser);

      test('AssetImageProvider - evicts on null load', () async {
        final Completer<StateError> error = Completer<StateError>();
        FlutterError.onError = (FlutterErrorDetails details) {
          error.complete(details.exception as StateError);
        };

        final ImageProvider provider = ExactAssetImage('does-not-exist', bundle: TestAssetBundle());
        final Object key = await provider.obtainKey(ImageConfiguration.empty);
        expect(imageCache.containsKey(key), false);

        provider.resolve(ImageConfiguration.empty);

        expect(imageCache.containsKey(key), true);

        await error.future;

        expect(imageCache.containsKey(key), false);
      }, skip: isBrowser);

      test('ImageProvider can evict images', () async {
        final Uint8List bytes = Uint8List.fromList(kTransparentImage);
        final MemoryImage imageProvider = MemoryImage(bytes);
        final ImageStream stream = imageProvider.resolve(ImageConfiguration.empty);
        final Completer<void> completer = Completer<void>();
        stream.addListener(ImageStreamListener((ImageInfo info, bool syncCall) => completer.complete()));
        await completer.future;

        expect(imageCache.currentSize, 1);
        expect(await MemoryImage(bytes).evict(), true);
        expect(imageCache.currentSize, 0);
      });

      test('ImageProvider.evict respects the provided ImageCache', () async {
        final ImageCache otherCache = ImageCache();
        final Uint8List bytes = Uint8List.fromList(kTransparentImage);
        final MemoryImage imageProvider = MemoryImage(bytes);
        final ImageStreamCompleter cacheStream = otherCache.putIfAbsent(
          imageProvider, () => imageProvider.load(imageProvider, basicDecoder),
        );
        final ImageStream stream = imageProvider.resolve(ImageConfiguration.empty);
        final Completer<void> completer = Completer<void>();
        final Completer<void> cacheCompleter = Completer<void>();
        stream.addListener(ImageStreamListener((ImageInfo info, bool syncCall) {
          completer.complete();
        }));
        cacheStream.addListener(ImageStreamListener((ImageInfo info, bool syncCall) {
          cacheCompleter.complete();
        }));
        await Future.wait(<Future<void>>[completer.future, cacheCompleter.future]);

        expect(otherCache.currentSize, 1);
        expect(imageCache.currentSize, 1);
        expect(await imageProvider.evict(cache: otherCache), true);
        expect(otherCache.currentSize, 0);
        expect(imageCache.currentSize, 1);
      });

      test('ImageProvider errors can always be caught', () async {
        final ErrorImageProvider imageProvider = ErrorImageProvider();
        final Completer<bool> caughtError = Completer<bool>();
        FlutterError.onError = (FlutterErrorDetails details) {
          caughtError.complete(false);
        };
        final ImageStream stream = imageProvider.resolve(ImageConfiguration.empty);
        stream.addListener(ImageStreamListener((ImageInfo info, bool syncCall) {
          caughtError.complete(false);
        }, onError: (dynamic error, StackTrace stackTrace) {
          caughtError.complete(true);
        }));
        expect(await caughtError.future, true);
      });
    });

    test('obtainKey errors will be caught', () async {
      final ImageProvider imageProvider = ObtainKeyErrorImageProvider();
      final Completer<bool> caughtError = Completer<bool>();
      FlutterError.onError = (FlutterErrorDetails details) {
        caughtError.complete(false);
      };
      final ImageStream stream = imageProvider.resolve(ImageConfiguration.empty);
      stream.addListener(ImageStreamListener((ImageInfo info, bool syncCall) {
        caughtError.complete(false);
      }, onError: (dynamic error, StackTrace stackTrace) {
        caughtError.complete(true);
      }));
      expect(await caughtError.future, true);
    });

    test('resolve sync errors will be caught', () async {
      bool uncaught = false;
      final Zone testZone = Zone.current.fork(specification: ZoneSpecification(
        handleUncaughtError: (Zone zone, ZoneDelegate zoneDelegate, Zone parent, Object error, StackTrace stackTrace) {
          uncaught = true;
        },
      ));
      await testZone.run(() async {
        final ImageProvider imageProvider = LoadErrorImageProvider();
        final Completer<bool> caughtError = Completer<bool>();
        FlutterError.onError = (FlutterErrorDetails details) {
          throw Error();
        };
        final ImageStream result = imageProvider.resolve(ImageConfiguration.empty);
        result.addListener(ImageStreamListener((ImageInfo info, bool syncCall) {
        }, onError: (dynamic error, StackTrace stackTrace) {
          caughtError.complete(true);
        }));
        expect(await caughtError.future, true);
      });
      expect(uncaught, false);
    });

    test('resolve errors in the completer will be caught', () async {
      bool uncaught = false;
      final Zone testZone = Zone.current.fork(specification: ZoneSpecification(
        handleUncaughtError: (Zone zone, ZoneDelegate zoneDelegate, Zone parent, Object error, StackTrace stackTrace) {
          uncaught = true;
        },
      ));
      await testZone.run(() async {
        final ImageProvider imageProvider = LoadErrorCompleterImageProvider();
        final Completer<bool> caughtError = Completer<bool>();
        FlutterError.onError = (FlutterErrorDetails details) {
          throw Error();
        };
        final ImageStream result = imageProvider.resolve(ImageConfiguration.empty);
        result.addListener(ImageStreamListener((ImageInfo info, bool syncCall) {
        }, onError: (dynamic error, StackTrace stackTrace) {
          caughtError.complete(true);
        }));
        expect(await caughtError.future, true);
      });
      expect(uncaught, false);
    });

    test('File image with empty file throws expected error and evicts from cache', () async {
      final Completer<StateError> error = Completer<StateError>();
      FlutterError.onError = (FlutterErrorDetails details) {
        print(details.exception);
        error.complete(details.exception as StateError);
      };
      final MemoryFileSystem fs = MemoryFileSystem();
      final File file = fs.file('/empty.png')..createSync(recursive: true);
      final FileImage provider = FileImage(file);

      expect(imageCache.containsKey(provider), false);

      provider.resolve(ImageConfiguration.empty);

      expect(imageCache.containsKey(provider), true);

      expect(await error.future, isStateError);
      expect(imageCache.containsKey(provider), false);
    });

    group(NetworkImage, () {
      MockHttpClient httpClient;

      setUp(() {
        httpClient = MockHttpClient();
        debugNetworkImageHttpClientProvider = () => httpClient;
      });

      tearDown(() {
        debugNetworkImageHttpClientProvider = null;
      });

      test('Expect thrown exception with statusCode - evicts from cache', () async {
        final int errorStatusCode = HttpStatus.notFound;
        const String requestUrl = 'foo-url';

        final MockHttpClientRequest request = MockHttpClientRequest();
        final MockHttpClientResponse response = MockHttpClientResponse();
        when(httpClient.getUrl(any)).thenAnswer((_) => Future<HttpClientRequest>.value(request));
        when(request.close()).thenAnswer((_) => Future<HttpClientResponse>.value(response));
        when(response.statusCode).thenReturn(errorStatusCode);

        final Completer<dynamic> caughtError = Completer<dynamic>();

        final ImageProvider imageProvider = NetworkImage(nonconst(requestUrl));
        expect(imageCache.containsKey(imageProvider), false);

        final ImageStream result = imageProvider.resolve(ImageConfiguration.empty);

        expect(imageCache.containsKey(imageProvider), true);

        result.addListener(ImageStreamListener((ImageInfo info, bool syncCall) {
        }, onError: (dynamic error, StackTrace stackTrace) {
          caughtError.complete(error);
        }));

        final dynamic err = await caughtError.future;

        expect(imageCache.containsKey(imageProvider), false);

        expect(
          err,
          isA<NetworkImageLoadException>()
            .having((NetworkImageLoadException e) => e.statusCode, 'statusCode', errorStatusCode)
            .having((NetworkImageLoadException e) => e.uri, 'uri', Uri.base.resolve(requestUrl)),
        );
      }, skip: isBrowser);  // Browser implementation does not use HTTP client but a <img> tag.

      test('Disallows null urls', () {
        expect(() {
          NetworkImage(nonconst(null));
        }, throwsAssertionError);
      });

      test('Uses the HttpClient provided by debugNetworkImageHttpClientProvider if set', () async {
        when(httpClient.getUrl(any)).thenThrow('client1');
        final List<dynamic> capturedErrors = <dynamic>[];

        Future<void> loadNetworkImage() async {
          final NetworkImage networkImage = NetworkImage(nonconst('foo'));
          final ImageStreamCompleter completer = networkImage.load(networkImage, basicDecoder);
          completer.addListener(ImageStreamListener(
            (ImageInfo image, bool synchronousCall) { },
            onError: (dynamic error, StackTrace stackTrace) {
              capturedErrors.add(error);
            },
          ));
          await Future<void>.value();
        }

        await loadNetworkImage();
        expect(capturedErrors, <dynamic>['client1']);
        final MockHttpClient client2 = MockHttpClient();
        when(client2.getUrl(any)).thenThrow('client2');
        debugNetworkImageHttpClientProvider = () => client2;
        await loadNetworkImage();
        expect(capturedErrors, <dynamic>['client1', 'client2']);
      }, skip: isBrowser);

      test('Propagates http client errors during resolve()', () async {
        when(httpClient.getUrl(any)).thenThrow(Error());
        bool uncaught = false;

        await runZoned(() async {
          const ImageProvider imageProvider = NetworkImage('asdasdasdas');
          final Completer<bool> caughtError = Completer<bool>();
          FlutterError.onError = (FlutterErrorDetails details) {
            throw Error();
          };
          final ImageStream result = imageProvider.resolve(ImageConfiguration.empty);
          result.addListener(ImageStreamListener((ImageInfo info, bool syncCall) {
          }, onError: (dynamic error, StackTrace stackTrace) {
            caughtError.complete(true);
          }));
          expect(await caughtError.future, true);
        }, zoneSpecification: ZoneSpecification(
          handleUncaughtError: (Zone zone, ZoneDelegate zoneDelegate, Zone parent, Object error, StackTrace stackTrace) {
            uncaught = true;
          },
        ));
        expect(uncaught, false);
      });

      test('Notifies listeners of chunk events', () async {
        const int chunkSize = 8;
        final List<Uint8List> chunks = <Uint8List>[
          for (int offset = 0; offset < kTransparentImage.length; offset += chunkSize)
            Uint8List.fromList(kTransparentImage.skip(offset).take(chunkSize).toList()),
        ];
        final Completer<void> imageAvailable = Completer<void>();
        final MockHttpClientRequest request = MockHttpClientRequest();
        final MockHttpClientResponse response = MockHttpClientResponse();
        when(httpClient.getUrl(any)).thenAnswer((_) => Future<HttpClientRequest>.value(request));
        when(request.close()).thenAnswer((_) => Future<HttpClientResponse>.value(response));
        when(response.statusCode).thenReturn(HttpStatus.ok);
        when(response.contentLength).thenReturn(kTransparentImage.length);
        when(response.listen(
          any,
          onDone: anyNamed('onDone'),
          onError: anyNamed('onError'),
          cancelOnError: anyNamed('cancelOnError'),
        )).thenAnswer((Invocation invocation) {
          final void Function(List<int>) onData = invocation.positionalArguments[0] as void Function(List<int>);
          final void Function(Object) onError = invocation.namedArguments[#onError] as void Function(Object);
          final VoidCallback onDone = invocation.namedArguments[#onDone] as VoidCallback;
          final bool cancelOnError = invocation.namedArguments[#cancelOnError] as bool;

          return Stream<Uint8List>.fromIterable(chunks).listen(
            onData,
            onDone: onDone,
            onError: onError,
            cancelOnError: cancelOnError,
          );
        });

        final ImageProvider imageProvider = NetworkImage(nonconst('foo'));
        final ImageStream result = imageProvider.resolve(ImageConfiguration.empty);
        final List<ImageChunkEvent> events = <ImageChunkEvent>[];
        result.addListener(ImageStreamListener(
          (ImageInfo image, bool synchronousCall) {
            imageAvailable.complete();
          },
          onChunk: (ImageChunkEvent event) {
            events.add(event);
          },
          onError: (dynamic error, StackTrace stackTrace) {
            imageAvailable.completeError(error, stackTrace);
          },
        ));
        await imageAvailable.future;
        expect(events.length, chunks.length);
        for (int i = 0; i < events.length; i++) {
          expect(events[i].cumulativeBytesLoaded, math.min((i + 1) * chunkSize, kTransparentImage.length));
          expect(events[i].expectedTotalBytes, kTransparentImage.length);
        }
      }, skip: isBrowser);
    });
  });

  test('ResizeImage resizes to the correct dimensions', () async {
    final Uint8List bytes = Uint8List.fromList(kTransparentImage);
    final MemoryImage imageProvider = MemoryImage(bytes);
    final Size rawImageSize = await _resolveAndGetSize(imageProvider);
    expect(rawImageSize, const Size(1, 1));

    const Size resizeDims = Size(14, 7);
    final ResizeImage resizedImage = ResizeImage(MemoryImage(bytes), width: resizeDims.width.round(), height: resizeDims.height.round());
    const ImageConfiguration resizeConfig = ImageConfiguration(size: resizeDims);
    final Size resizedImageSize = await _resolveAndGetSize(resizedImage, configuration: resizeConfig);
    expect(resizedImageSize, resizeDims);
  }, skip: isBrowser);

  test('ResizeImage does not resize when no size is passed', () async {
    final Uint8List bytes = Uint8List.fromList(kTransparentImage);
    final MemoryImage imageProvider = MemoryImage(bytes);
    final Size rawImageSize = await _resolveAndGetSize(imageProvider);
    expect(rawImageSize, const Size(1, 1));

    // Cannot pass in two null arguments for cache dimensions, so will use the regular
    // MemoryImage
    final MemoryImage resizedImage = MemoryImage(bytes);
    final Size resizedImageSize = await _resolveAndGetSize(resizedImage);
    expect(resizedImageSize, const Size(1, 1));
  }, skip: isBrowser);

  test('ResizeImage stores values', () async {
    final Uint8List bytes = Uint8List.fromList(kTransparentImage);
    final MemoryImage memoryImage = MemoryImage(bytes);
    final ResizeImage resizeImage = ResizeImage(memoryImage, width: 10, height: 20);
    expect(resizeImage.width, 10);
    expect(resizeImage.height, 20);
    expect(resizeImage.imageProvider, memoryImage);

    expect(memoryImage.resolve(ImageConfiguration.empty) != resizeImage.resolve(ImageConfiguration.empty), true);
  });

  test('ResizeImage takes one dim', () async {
    final Uint8List bytes = Uint8List.fromList(kTransparentImage);
    final MemoryImage memoryImage = MemoryImage(bytes);
    final ResizeImage resizeImage = ResizeImage(memoryImage, width: 10, height: null);
    expect(resizeImage.width, 10);
    expect(resizeImage.height, null);
    expect(resizeImage.imageProvider, memoryImage);

    expect(memoryImage.resolve(ImageConfiguration.empty) != resizeImage.resolve(ImageConfiguration.empty), true);
  });

  test('ResizeImage forms closure', () async {
    final Uint8List bytes = Uint8List.fromList(kTransparentImage);
    final MemoryImage memoryImage = MemoryImage(bytes);
    final ResizeImage resizeImage = ResizeImage(memoryImage, width: 123, height: 321);

    final DecoderCallback decode = (Uint8List bytes, {int cacheWidth, int cacheHeight}) {
      expect(cacheWidth, 123);
      expect(cacheHeight, 321);
      return PaintingBinding.instance.instantiateImageCodec(bytes, cacheWidth: cacheWidth, cacheHeight: cacheHeight);
    };

    resizeImage.load(await resizeImage.obtainKey(ImageConfiguration.empty), decode);
  });

  test('ResizeImage handles sync obtainKey', () async {
    final Uint8List bytes = Uint8List.fromList(kTransparentImage);
    final MemoryImage memoryImage = MemoryImage(bytes);
    final ResizeImage resizeImage = ResizeImage(memoryImage, width: 123, height: 321);

    bool isAsync = false;
    resizeImage.obtainKey(ImageConfiguration.empty).then((Object key) {
      expect(isAsync, false);
    });
    isAsync = true;
    expect(isAsync, true);
  });

  test('ResizeImage handles async obtainKey', () async {
    final Uint8List bytes = Uint8List.fromList(kTransparentImage);
    final AsyncKeyMemoryImage memoryImage = AsyncKeyMemoryImage(bytes);
    final ResizeImage resizeImage = ResizeImage(memoryImage, width: 123, height: 321);

    bool isAsync = false;
    resizeImage.obtainKey(ImageConfiguration.empty).then((Object key) {
      expect(isAsync, true);
    });
    isAsync = true;
    expect(isAsync, true);
  });

  test('File image with empty file throws expected error (load)', () async {
    final Completer<StateError> error = Completer<StateError>();
    FlutterError.onError = (FlutterErrorDetails details) {
      error.complete(details.exception as StateError);
    };
    final MemoryFileSystem fs = MemoryFileSystem();
    final File file = fs.file('/empty.png')..createSync(recursive: true);
    final FileImage provider = FileImage(file);

    expect(provider.load(provider, null), isA<MultiFrameImageStreamCompleter>());

    expect(await error.future, isStateError);
  });
}

Future<Size> _resolveAndGetSize(ImageProvider imageProvider,
    {ImageConfiguration configuration = ImageConfiguration.empty}) async {
  final ImageStream stream = imageProvider.resolve(configuration);
  final Completer<Size> completer = Completer<Size>();
  final ImageStreamListener listener =
    ImageStreamListener((ImageInfo image, bool synchronousCall) {
      final int height = image.image.height;
      final int width = image.image.width;
      completer.complete(Size(width.toDouble(), height.toDouble()));
    }
  );
  stream.addListener(listener);
  return await completer.future;
}

// This version of MemoryImage guarantees obtainKey returns a future that has not been
// completed synchronously.
class AsyncKeyMemoryImage extends MemoryImage {
  AsyncKeyMemoryImage(Uint8List bytes) : super(bytes);

  @override
  Future<MemoryImage> obtainKey(ImageConfiguration configuration) {
    return Future<MemoryImage>(() => this);
  }
}

class MockHttpClient extends Mock implements HttpClient {}
class MockHttpClientRequest extends Mock implements HttpClientRequest {}
class MockHttpClientResponse extends Mock implements HttpClientResponse {}

class TestAssetBundle extends CachingAssetBundle {
  @override
  Future<ByteData> load(String key) async {
    return null;
  }
}