// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:async';
import 'dart:ui';

import 'package:flutter/painting.dart';
import 'package:flutter/scheduler.dart' show timeDilation;
import 'package:flutter_test/flutter_test.dart';

class FakeFrameInfo extends FrameInfo {
  final Duration _duration;
  final Image _image;

  FakeFrameInfo(int width, int height, this._duration) :
    _image = new FakeImage(width, height);

  @override
  Duration get duration => _duration;

  @override
  Image get image => _image;
}

class FakeImage extends Image {
  final int _width;
  final int _height;

  FakeImage(this._width, this._height);

  @override
  int get width => _width;

  @override
  int get height => _height;

  @override
  void dispose() {}
}

class MockCodec implements Codec {

  @override
  int frameCount;

  @override
  int repetitionCount;

  int numFramesAsked = 0;

  Completer<FrameInfo> _nextFrameCompleter = new Completer<FrameInfo>();

  @override
  Future<FrameInfo> getNextFrame() {
    numFramesAsked += 1;
    return _nextFrameCompleter.future;
  }

  void completeNextFrame(FrameInfo frameInfo) {
    _nextFrameCompleter.complete(frameInfo);
    _nextFrameCompleter = new Completer<FrameInfo>();
  }

  void failNextFrame(String err) {
    _nextFrameCompleter.completeError(err);
  }

  @override
  void dispose() {}

}

void main() {
  testWidgets('Codec future fails', (WidgetTester tester) async {
    final Completer<Codec> completer = new Completer<Codec>();
    new MultiFrameImageStreamCompleter(
      codec: completer.future,
      scale: 1.0,
    );
    completer.completeError('failure message');
    await tester.idle();
    expect(tester.takeException(), 'failure message');
  });

  testWidgets('First frame decoding starts when codec is ready', (WidgetTester tester) async {
    final Completer<Codec> completer = new Completer<Codec>();
    final MockCodec mockCodec = new MockCodec();
    mockCodec.frameCount = 1;
    new MultiFrameImageStreamCompleter(
      codec: completer.future,
      scale: 1.0,
    );

    completer.complete(mockCodec);
    await tester.idle();
    expect(mockCodec.numFramesAsked, 1);
  });

   testWidgets('getNextFrame future fails', (WidgetTester tester) async {
     final MockCodec mockCodec = new MockCodec();
     mockCodec.frameCount = 1;
     final Completer<Codec> codecCompleter = new Completer<Codec>();

     new MultiFrameImageStreamCompleter(
       codec: codecCompleter.future,
       scale: 1.0,
     );

     codecCompleter.complete(mockCodec);
     // MultiFrameImageStreamCompleter only sets an error handler for the next
     // frame future after the codec future has completed.
     // Idling here lets the MultiFrameImageStreamCompleter advance and set the
     // error handler for the nextFrame future.
     await tester.idle();

     mockCodec.failNextFrame('frame completion error');
     await tester.idle();

     expect(tester.takeException(), 'frame completion error');
   });

  testWidgets('ImageStream emits frame (static image)', (WidgetTester tester) async {
    final MockCodec mockCodec = new MockCodec();
    mockCodec.frameCount = 1;
    final Completer<Codec> codecCompleter = new Completer<Codec>();

    final ImageStreamCompleter imageStream = new MultiFrameImageStreamCompleter(
      codec: codecCompleter.future,
      scale: 1.0,
    );

    final List<ImageInfo> emittedImages = <ImageInfo>[];
    imageStream.addListener((ImageInfo image, bool synchronousCall) {
      emittedImages.add(image);
    });

    codecCompleter.complete(mockCodec);
    await tester.idle();

    final FrameInfo frame = new FakeFrameInfo(20, 10, const Duration(milliseconds: 200));
    mockCodec.completeNextFrame(frame);
    await tester.idle();

    expect(emittedImages, equals(<ImageInfo>[new ImageInfo(image: frame.image)]));
  });

   testWidgets('ImageStream emits frames (animated images)', (WidgetTester tester) async {
     final MockCodec mockCodec = new MockCodec();
     mockCodec.frameCount = 2;
     mockCodec.repetitionCount = -1;
     final Completer<Codec> codecCompleter = new Completer<Codec>();

     final ImageStreamCompleter imageStream = new MultiFrameImageStreamCompleter(
       codec: codecCompleter.future,
       scale: 1.0,
     );

     final List<ImageInfo> emittedImages = <ImageInfo>[];
     imageStream.addListener((ImageInfo image, bool synchronousCall) {
       emittedImages.add(image);
     });

     codecCompleter.complete(mockCodec);
     await tester.idle();

     final FrameInfo frame1 = new FakeFrameInfo(20, 10, const Duration(milliseconds: 200));
     mockCodec.completeNextFrame(frame1);
     await tester.idle();
     // We are waiting for the next animation tick, so at this point no frames
     // should have been emitted.
     expect(emittedImages.length, 0);

     await tester.pump();
     expect(emittedImages, equals(<ImageInfo>[new ImageInfo(image: frame1.image)]));

     final FrameInfo frame2 = new FakeFrameInfo(200, 100, const Duration(milliseconds: 400));
     mockCodec.completeNextFrame(frame2);

     await tester.pump(const Duration(milliseconds: 100));
     // The duration for the current frame was 200ms, so we don't emit the next
     // frame yet even though it is ready.
     expect(emittedImages.length, 1);

     await tester.pump(const Duration(milliseconds: 100));
     expect(emittedImages, equals(<ImageInfo>[
       new ImageInfo(image: frame1.image),
       new ImageInfo(image: frame2.image),
     ]));

     // Let the pending timer for the next frame to complete so we can cleanly
     // quit the test without pending timers.
     await tester.pump(const Duration(milliseconds: 400));
   });

   testWidgets('animation wraps back', (WidgetTester tester) async {
     final MockCodec mockCodec = new MockCodec();
     mockCodec.frameCount = 2;
     mockCodec.repetitionCount = -1;
     final Completer<Codec> codecCompleter = new Completer<Codec>();

     final ImageStreamCompleter imageStream = new MultiFrameImageStreamCompleter(
       codec: codecCompleter.future,
       scale: 1.0,
     );

     final List<ImageInfo> emittedImages = <ImageInfo>[];
     imageStream.addListener((ImageInfo image, bool synchronousCall) {
       emittedImages.add(image);
     });

     codecCompleter.complete(mockCodec);
     await tester.idle();

     final FrameInfo frame1 = new FakeFrameInfo(20, 10, const Duration(milliseconds: 200));
     final FrameInfo frame2 = new FakeFrameInfo(200, 100, const Duration(milliseconds: 400));

     mockCodec.completeNextFrame(frame1);
     await tester.idle(); // let nextFrameFuture complete
     await tester.pump(); // first animation frame shows on first app frame.
     mockCodec.completeNextFrame(frame2);
     await tester.idle(); // let nextFrameFuture complete
     await tester.pump(const Duration(milliseconds: 200)); // emit 2nd frame.
     mockCodec.completeNextFrame(frame1);
     await tester.idle(); // let nextFrameFuture complete
     await tester.pump(const Duration(milliseconds: 400)); // emit 3rd frame

     expect(emittedImages, equals(<ImageInfo>[
       new ImageInfo(image: frame1.image),
       new ImageInfo(image: frame2.image),
       new ImageInfo(image: frame1.image),
     ]));

     // Let the pending timer for the next frame to complete so we can cleanly
     // quit the test without pending timers.
     await tester.pump(const Duration(milliseconds: 200));
   });

   testWidgets('animation doesnt repeat more than specified', (WidgetTester tester) async {
     final MockCodec mockCodec = new MockCodec();
     mockCodec.frameCount = 2;
     mockCodec.repetitionCount = 0;
     final Completer<Codec> codecCompleter = new Completer<Codec>();

     final ImageStreamCompleter imageStream = new MultiFrameImageStreamCompleter(
       codec: codecCompleter.future,
       scale: 1.0,
     );

     final List<ImageInfo> emittedImages = <ImageInfo>[];
     imageStream.addListener((ImageInfo image, bool synchronousCall) {
       emittedImages.add(image);
     });

     codecCompleter.complete(mockCodec);
     await tester.idle();

     final FrameInfo frame1 = new FakeFrameInfo(20, 10, const Duration(milliseconds: 200));
     final FrameInfo frame2 = new FakeFrameInfo(200, 100, const Duration(milliseconds: 400));

     mockCodec.completeNextFrame(frame1);
     await tester.idle(); // let nextFrameFuture complete
     await tester.pump(); // first animation frame shows on first app frame.
     mockCodec.completeNextFrame(frame2);
     await tester.idle(); // let nextFrameFuture complete
     await tester.pump(const Duration(milliseconds: 200)); // emit 2nd frame.
     mockCodec.completeNextFrame(frame1);
     // allow another frame to complete (but we shouldn't be asking for it as
     // this animation should not repeat.
     await tester.idle();
     await tester.pump(const Duration(milliseconds: 400));

     expect(emittedImages, equals(<ImageInfo>[
       new ImageInfo(image: frame1.image),
       new ImageInfo(image: frame2.image),
     ]));
   });

   testWidgets('frames are only decoded when there are active listeners', (WidgetTester tester) async {
     final MockCodec mockCodec = new MockCodec();
     mockCodec.frameCount = 2;
     mockCodec.repetitionCount = -1;
     final Completer<Codec> codecCompleter = new Completer<Codec>();

     final ImageStreamCompleter imageStream = new MultiFrameImageStreamCompleter(
       codec: codecCompleter.future,
       scale: 1.0,
     );

     final ImageListener listener = (ImageInfo image, bool synchronousCall) {};
     imageStream.addListener(listener);

     codecCompleter.complete(mockCodec);
     await tester.idle();

     final FrameInfo frame1 = new FakeFrameInfo(20, 10, const Duration(milliseconds: 200));
     final FrameInfo frame2 = new FakeFrameInfo(200, 100, const Duration(milliseconds: 400));

     mockCodec.completeNextFrame(frame1);
     await tester.idle(); // let nextFrameFuture complete
     await tester.pump(); // first animation frame shows on first app frame.
     mockCodec.completeNextFrame(frame2);
     imageStream.removeListener(listener);
     await tester.idle(); // let nextFrameFuture complete
     await tester.pump(const Duration(milliseconds: 400)); // emit 2nd frame.

     // Decoding of the 3rd frame should not start as there are no registered
     // listeners to the stream
     expect(mockCodec.numFramesAsked, 2);

     imageStream.addListener(listener);
     await tester.idle(); // let nextFrameFuture complete
     expect(mockCodec.numFramesAsked, 3);
   });

   testWidgets('multiple stream listeners', (WidgetTester tester) async {
     final MockCodec mockCodec = new MockCodec();
     mockCodec.frameCount = 2;
     mockCodec.repetitionCount = -1;
     final Completer<Codec> codecCompleter = new Completer<Codec>();

     final ImageStreamCompleter imageStream = new MultiFrameImageStreamCompleter(
       codec: codecCompleter.future,
       scale: 1.0,
     );

     final List<ImageInfo> emittedImages1 = <ImageInfo>[];
     final ImageListener listener1 = (ImageInfo image, bool synchronousCall) {
       emittedImages1.add(image);
     };
     final List<ImageInfo> emittedImages2 = <ImageInfo>[];
     final ImageListener listener2 = (ImageInfo image, bool synchronousCall) {
       emittedImages2.add(image);
     };
     imageStream.addListener(listener1);
     imageStream.addListener(listener2);

     codecCompleter.complete(mockCodec);
     await tester.idle();

     final FrameInfo frame1 = new FakeFrameInfo(20, 10, const Duration(milliseconds: 200));
     final FrameInfo frame2 = new FakeFrameInfo(200, 100, const Duration(milliseconds: 400));

     mockCodec.completeNextFrame(frame1);
     await tester.idle(); // let nextFrameFuture complete
     await tester.pump(); // first animation frame shows on first app frame.
     expect(emittedImages1, equals(<ImageInfo>[new ImageInfo(image: frame1.image)]));
     expect(emittedImages2, equals(<ImageInfo>[new ImageInfo(image: frame1.image)]));

     mockCodec.completeNextFrame(frame2);
     await tester.idle(); // let nextFrameFuture complete
     await tester.pump(); // next app frame will schedule a timer.
     imageStream.removeListener(listener1);

     await tester.pump(const Duration(milliseconds: 400)); // emit 2nd frame.
     expect(emittedImages1, equals(<ImageInfo>[new ImageInfo(image: frame1.image)]));
     expect(emittedImages2, equals(<ImageInfo>[
       new ImageInfo(image: frame1.image),
       new ImageInfo(image: frame2.image),
     ]));
   });

   testWidgets('timer is canceled when listeners are removed', (WidgetTester tester) async {
     final MockCodec mockCodec = new MockCodec();
     mockCodec.frameCount = 2;
     mockCodec.repetitionCount = -1;
     final Completer<Codec> codecCompleter = new Completer<Codec>();

     final ImageStreamCompleter imageStream = new MultiFrameImageStreamCompleter(
       codec: codecCompleter.future,
       scale: 1.0,
     );

     final ImageListener listener = (ImageInfo image, bool synchronousCall) {};
     imageStream.addListener(listener);

     codecCompleter.complete(mockCodec);
     await tester.idle();

     final FrameInfo frame1 = new FakeFrameInfo(20, 10, const Duration(milliseconds: 200));
     final FrameInfo frame2 = new FakeFrameInfo(200, 100, const Duration(milliseconds: 400));

     mockCodec.completeNextFrame(frame1);
     await tester.idle(); // let nextFrameFuture complete
     await tester.pump(); // first animation frame shows on first app frame.

     mockCodec.completeNextFrame(frame2);
     await tester.idle(); // let nextFrameFuture complete
     await tester.pump();

     imageStream.removeListener(listener);
     // The test framework will fail this if there are pending timers at this
     // point.
   });

   testWidgets('timeDilation affects animation frame timers', (WidgetTester tester) async {
     final MockCodec mockCodec = new MockCodec();
     mockCodec.frameCount = 2;
     mockCodec.repetitionCount = -1;
     final Completer<Codec> codecCompleter = new Completer<Codec>();

     final ImageStreamCompleter imageStream = new MultiFrameImageStreamCompleter(
       codec: codecCompleter.future,
       scale: 1.0,
     );

     final ImageListener listener = (ImageInfo image, bool synchronousCall) {};
     imageStream.addListener(listener);

     codecCompleter.complete(mockCodec);
     await tester.idle();

     final FrameInfo frame1 = new FakeFrameInfo(20, 10, const Duration(milliseconds: 200));
     final FrameInfo frame2 = new FakeFrameInfo(200, 100, const Duration(milliseconds: 400));

     mockCodec.completeNextFrame(frame1);
     await tester.idle(); // let nextFrameFuture complete
     await tester.pump(); // first animation frame shows on first app frame.

     timeDilation = 2.0;
     mockCodec.completeNextFrame(frame2);
     await tester.idle(); // let nextFrameFuture complete
     await tester.pump(); // schedule next app frame
     await tester.pump(const Duration(milliseconds: 200)); // emit 2nd frame.
     // Decoding of the 3rd frame should not start after 200 ms, as time is
     // dilated by a factor of 2.
     expect(mockCodec.numFramesAsked, 2);
     await tester.pump(const Duration(milliseconds: 200)); // emit 2nd frame.
     expect(mockCodec.numFramesAsked, 3);

   });
}
