// Copyright 2014 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import 'dart:async';
import 'dart:collection';

import 'package:clock/clock.dart';
import 'package:collection/collection.dart';

/// The type of a microtask callback.
typedef _Microtask = void Function();

/// Runs [callback] in a [Zone] where all asynchrony is controlled by an
/// instance of [FakeAsync].
///
/// All [Future]s, [Stream]s, [Timer]s, microtasks, and other time-based
/// asynchronous features used within [callback] are controlled by calls to
/// [FakeAsync.elapse] rather than the passing of real time.
///
/// The [`clock`][] property will be set to a clock that reports the fake
/// elapsed time. By default, it starts at the time [fakeAsync] was created
/// (according to [`clock.now()`][]), but this can be controlled by passing
/// [initialTime].
///
/// [`clock`]: https://www.dartdocs.org/documentation/clock/latest/clock/clock.html
/// [`clock.now()`]: https://www.dartdocs.org/documentation/clock/latest/clock/Clock/now.html
///
/// Returns the result of [callback].
T fakeAsync<T>(T Function(FakeAsync async) callback, {DateTime? initialTime}) =>
    FakeAsync(initialTime: initialTime).run(callback);

/// A class that mocks out the passage of time within a [Zone].
///
/// Test code can be passed as a callback to [run], which causes it to be run in
/// a [Zone] which fakes timer and microtask creation, such that they are run
/// during calls to [elapse] which simulates the asynchronous passage of time.
///
/// The synchronous passage of time (as from blocking or expensive calls) can
/// also be simulated using [elapseBlocking].
class FakeAsync {
  /// The value of [clock] within [run].
  late final Clock _clock;

  /// The amount of fake time that's elapsed since this [FakeAsync] was
  /// created.
  Duration get elapsed => _elapsed;
  var _elapsed = Duration.zero;

  /// Whether Timers created by this FakeAsync will include a creation stack
  /// trace in [FakeAsync.pendingTimersDebugString].
  final bool includeTimerStackTrace;

  /// The fake time at which the current call to [elapse] will finish running.
  ///
  /// This is `null` if there's no current call to [elapse].
  Duration? _elapsingTo;

  /// Tasks that are scheduled to run when fake time progresses.
  final _microtasks = Queue<_Microtask>();

  /// All timers created within [run].
  final _timers = <FakeTimer>{};

  /// All the current pending timers.
  List<FakeTimer> get pendingTimers => _timers.toList(growable: false);

  /// The debug strings for all the current pending timers.
  List<String> get pendingTimersDebugString =>
      pendingTimers.map((timer) => timer.debugString).toList(growable: false);

  /// The number of active periodic timers created within a call to [run] or
  /// [fakeAsync].
  int get periodicTimerCount =>
      _timers.where((timer) => timer.isPeriodic).length;

  /// The number of active non-periodic timers created within a call to [run] or
  /// [fakeAsync].
  int get nonPeriodicTimerCount =>
      _timers.where((timer) => !timer.isPeriodic).length;

  /// The number of pending microtasks scheduled within a call to [run] or
  /// [fakeAsync].
  int get microtaskCount => _microtasks.length;

  /// Creates a [FakeAsync].
  ///
  /// Within [run], the [`clock`][] property will start at [initialTime] and
  /// move forward as fake time elapses.
  ///
  /// [`clock`]: https://www.dartdocs.org/documentation/clock/latest/clock/clock.html
  ///
  /// Note: it's usually more convenient to use [fakeAsync] rather than creating
  /// a [FakeAsync] object and calling [run] manually.
  FakeAsync({DateTime? initialTime, this.includeTimerStackTrace = true}) {
    final nonNullInitialTime = initialTime ?? clock.now();
    _clock = Clock(() => nonNullInitialTime.add(elapsed));
  }

  /// Returns a fake [Clock] whose time can is elapsed by calls to [elapse] and
  /// [elapseBlocking].
  ///
  /// The returned clock starts at [initialTime] plus the fake time that's
  /// already been elapsed. Further calls to [elapse] and [elapseBlocking] will
  /// advance the clock as well.
  ///
  /// Note that it's usually easier to use the top-level [`clock`][] property.
  /// Only call this function if you want a different [initialTime] than the
  /// default.
  ///
  /// [`clock`]: https://www.dartdocs.org/documentation/clock/latest/clock/clock.html
  Clock getClock(DateTime initialTime) =>
      Clock(() => initialTime.add(_elapsed));

  /// Simulates the asynchronous passage of time.
  ///
  /// Throws an [ArgumentError] if [duration] is negative. Throws a [StateError]
  /// if a previous call to [elapse] has not yet completed.
  ///
  /// Any timers created within [run] or [fakeAsync] will fire if their time is
  /// within [duration]. The microtask queue is processed before and after each
  /// timer fires.
  void elapse(Duration duration) {
    if (duration.inMicroseconds < 0) {
      throw ArgumentError.value(duration, 'duration', 'may not be negative');
    } else if (_elapsingTo != null) {
      throw StateError('Cannot elapse until previous elapse is complete.');
    }

    _elapsingTo = _elapsed + duration;
    _fireTimersWhile((next) => next._nextCall <= _elapsingTo!);
    _elapseTo(_elapsingTo!);
    _elapsingTo = null;
  }

  /// Simulates the synchronous passage of time, resulting from blocking or
  /// expensive calls.
  ///
  /// Neither timers nor microtasks are run during this call, but if this is
  /// called within [elapse] they may fire afterwards.
  ///
  /// Throws an [ArgumentError] if [duration] is negative.
  void elapseBlocking(Duration duration) {
    if (duration.inMicroseconds < 0) {
      throw ArgumentError.value(duration, 'duration', 'Must not be negative');
    }

    _elapsed += duration;
    final elapsingTo = _elapsingTo;
    if (elapsingTo != null && _elapsed > elapsingTo) _elapsingTo = _elapsed;
  }

  /// Runs [callback] in a [Zone] where all asynchrony is controlled by `this`.
  ///
  /// All [Future]s, [Stream]s, [Timer]s, microtasks, and other time-based
  /// asynchronous features used within [callback] are controlled by calls to
  /// [elapse] rather than the passing of real time.
  ///
  /// The [`clock`][] property will be set to a clock that reports the fake
  /// elapsed time. By default, it starts at the time the [FakeAsync] was
  /// created (according to [`clock.now()`][]), but this can be controlled by
  /// passing `initialTime` to [FakeAsync.new].
  ///
  /// [`clock`]: https://www.dartdocs.org/documentation/clock/latest/clock/clock.html
  /// [`clock.now()`]: https://www.dartdocs.org/documentation/clock/latest/clock/Clock/now.html
  ///
  /// Calls [callback] with `this` as argument and returns its result.
  ///
  /// Note: it's usually more convenient to use [fakeAsync] rather than creating
  /// a [FakeAsync] object and calling [run] manually.
  T run<T>(T Function(FakeAsync self) callback) => runZoned(
        () => withClock(_clock, () => callback(this)),
        zoneSpecification: ZoneSpecification(
          createTimer: (_, __, zone, duration, callback) =>
              _createTimer(duration, zone, callback, false),
          createPeriodicTimer: (_, __, zone, duration, callback) =>
              _createTimer(duration, zone, callback, true),
          scheduleMicrotask: (_, __, zone, microtask) => _microtasks.add(() {
            zone.runGuarded(microtask);
          }),
        ),
      );

  /// Runs all pending microtasks scheduled within a call to [run] or
  /// [fakeAsync] until there are no more microtasks scheduled.
  ///
  /// Does not run timers.
  void flushMicrotasks() {
    while (_microtasks.isNotEmpty) {
      _microtasks.removeFirst()();
    }
  }

  /// Elapses time until there are no more active timers.
  ///
  /// If `flushPeriodicTimers` is `true` (the default), this will repeatedly run
  /// periodic timers until they're explicitly canceled. Otherwise, this will
  /// stop when the only active timers are periodic.
  ///
  /// The [timeout] controls how much fake time may elapse before a [StateError]
  /// is thrown. This ensures that a periodic timer doesn't cause this method to
  /// deadlock. It defaults to one hour.
  void flushTimers({
    Duration timeout = const Duration(hours: 1),
    bool flushPeriodicTimers = true,
  }) {
    final absoluteTimeout = _elapsed + timeout;
    _fireTimersWhile((timer) {
      if (timer._nextCall > absoluteTimeout) {
        // TODO(nweiz): Make this a [TimeoutException].
        throw StateError('Exceeded timeout $timeout while flushing timers');
      }

      // Always run timer if it's due.
      if (timer._nextCall <= elapsed) return true;

      // If no timers are due, continue running timers
      // (and advancing time to their next due time)
      // if flushing periodic timers,
      // or if there is any non-periodic timer left.
      return flushPeriodicTimers || _timers.any((timer) => !timer.isPeriodic);
    });
  }

  /// Invoke the callback for each timer until [predicate] returns `false` for
  /// the next timer that would be fired.
  ///
  /// Microtasks are flushed before and after each timer is fired. Before each
  /// timer fires, [_elapsed] is updated to the appropriate duration.
  void _fireTimersWhile(bool Function(FakeTimer timer) predicate) {
    flushMicrotasks();
    while (_timers.isNotEmpty) {
      final timer = minBy(_timers, (FakeTimer timer) => timer._nextCall)!;
      if (!predicate(timer)) break;

      _elapseTo(timer._nextCall);
      timer._fire();
      flushMicrotasks();
    }
  }

  /// Creates a new timer controlled by `this` that fires [callback] after
  /// [duration] (or every [duration] if [periodic] is `true`).
  Timer _createTimer(
    Duration duration,
    Zone zone,
    Function callback,
    bool periodic,
  ) {
    final timer = FakeTimer._(
      duration,
      zone,
      callback,
      periodic,
      this,
      includeStackTrace: includeTimerStackTrace,
    );
    _timers.add(timer);
    return timer;
  }

  /// Sets [_elapsed] to [to] if [to] is longer than [_elapsed].
  void _elapseTo(Duration to) {
    if (to > _elapsed) _elapsed = to;
  }
}

/// An implementation of [Timer] that's controlled by a [FakeAsync].
///
/// Periodic timers attempt to be isochronous. They trigger as soon as possible
/// after a multiple of the [duration] has passed since they started,
/// independently of when prior callbacks actually ran.
/// This behavior matches VM timers.
///
/// Repeating web timers instead reschedule themselves a [duration] after
/// their last callback ended, which shifts the timing both if a callback
/// is delayed or if it runs for a long time. In return it guarantees
/// that there is always at least [duration] between two callbacks.
class FakeTimer implements Timer {
  /// The zone to run the callback in.
  final Zone _zone;

  /// If this is periodic, the time that should elapse between firings of this
  /// timer.
  ///
  /// This is not used by non-periodic timers.
  final Duration duration;

  /// The callback to invoke when the timer fires.
  ///
  /// For periodic timers, this is a `void Function(Timer)`. For non-periodic
  /// timers, it's a `void Function()`.
  final Function _callback;

  /// Whether this is a periodic timer.
  final bool isPeriodic;

  /// The [FakeAsync] instance that controls this timer.
  final FakeAsync _async;

  /// The value of [FakeAsync._elapsed] at (or after) which this timer should be
  /// fired.
  Duration _nextCall;

  /// The current stack trace when this timer was created.
  ///
  /// If [FakeAsync.includeTimerStackTrace] is set to false then accessing
  /// this field will throw a [TypeError].
  StackTrace get creationStackTrace => _creationStackTrace!;
  final StackTrace? _creationStackTrace;

  var _tick = 0;

  @override
  int get tick => _tick;

  /// Returns debugging information to try to identify the source of the
  /// [Timer].
  String get debugString => 'Timer (duration: $duration, periodic: $isPeriodic)'
      '${_creationStackTrace != null ? ', created:\n$creationStackTrace' : ''}';

  FakeTimer._(
    Duration duration,
    this._zone,
    this._callback,
    this.isPeriodic,
    this._async, {
    bool includeStackTrace = true,
  })  : duration =
            duration < Duration.zero ? (duration = Duration.zero) : duration,
        _nextCall = _async._elapsed + duration,
        _creationStackTrace = includeStackTrace ? StackTrace.current : null;

  @override
  bool get isActive => _async._timers.contains(this);

  @override
  void cancel() => _async._timers.remove(this);

  /// Fires this timer's callback and updates its state as necessary.
  void _fire() {
    assert(isActive);
    assert(_nextCall <= _async._elapsed);
    _tick++;
    if (isPeriodic) {
      _nextCall += duration;
      while (_nextCall < _async._elapsed) {
        _tick++;
        _nextCall += duration;
      }
      _zone.runUnaryGuarded(_callback as void Function(Timer), this);
    } else {
      cancel();
      _zone.runGuarded(_callback as void Function());
    }
  }
}
