// Copyright (c) 2018, the Dart project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

// ignore_for_file: only_throw_errors, inference_failure_on_instance_creation

import 'dart:async';

import 'package:test/test.dart';
import 'package:timing/src/clock.dart';
import 'package:timing/src/timing.dart';

void _noop() {}

void main() {
  late DateTime time;
  final startTime = DateTime(2017);
  DateTime fakeClock() => time;

  late TimeTracker tracker;
  late TimeTracker nestedTracker;

  T scopedTrack<T>(T Function() f) =>
      scopeClock(fakeClock, () => tracker.track(f));

  setUp(() {
    time = startTime;
  });

  void canHandleSync([void Function() additionalExpects = _noop]) {
    test('Can track sync code', () {
      expect(tracker.isStarted, false);
      expect(tracker.isTracking, false);
      expect(tracker.isFinished, false);
      scopedTrack(() {
        expect(tracker.isStarted, true);
        expect(tracker.isTracking, true);
        expect(tracker.isFinished, false);
        time = time.add(const Duration(seconds: 5));
      });
      expect(tracker.isStarted, true);
      expect(tracker.isTracking, false);
      expect(tracker.isFinished, true);
      expect(tracker.startTime, startTime);
      expect(tracker.stopTime, time);
      expect(tracker.duration, const Duration(seconds: 5));
      additionalExpects();
    });

    test('Can track handled sync exceptions', () async {
      scopedTrack(() {
        try {
          time = time.add(const Duration(seconds: 4));
          throw 'error';
        } on String {
          time = time.add(const Duration(seconds: 1));
        }
      });
      expect(tracker.isFinished, true);
      expect(tracker.startTime, startTime);
      expect(tracker.stopTime, time);
      expect(tracker.duration, const Duration(seconds: 5));
      additionalExpects();
    });

    test('Can track in case of unhandled sync exceptions', () async {
      expect(
          () => scopedTrack(() {
                time = time.add(const Duration(seconds: 5));
                throw 'error';
              }),
          throwsA(const TypeMatcher<String>()));
      expect(tracker.startTime, startTime);
      expect(tracker.stopTime, time);
      expect(tracker.duration, const Duration(seconds: 5));
      additionalExpects();
    });

    test('Can be nested sync', () {
      scopedTrack(() {
        time = time.add(const Duration(seconds: 1));
        nestedTracker.track(() {
          time = time.add(const Duration(seconds: 2));
        });
        time = time.add(const Duration(seconds: 4));
      });
      expect(tracker.isFinished, true);
      expect(tracker.startTime, startTime);
      expect(tracker.stopTime, time);
      expect(tracker.duration, const Duration(seconds: 7));
      expect(nestedTracker.startTime.isAfter(startTime), true);
      expect(nestedTracker.stopTime.isBefore(time), true);
      expect(nestedTracker.duration, const Duration(seconds: 2));
      additionalExpects();
    });
  }

  void canHandleAsync([void Function() additionalExpects = _noop]) {
    test('Can track async code', () async {
      expect(tracker.isStarted, false);
      expect(tracker.isTracking, false);
      expect(tracker.isFinished, false);
      await scopedTrack(() => Future(() {
            expect(tracker.isStarted, true);
            expect(tracker.isTracking, true);
            expect(tracker.isFinished, false);
            time = time.add(const Duration(seconds: 5));
          }));
      expect(tracker.isStarted, true);
      expect(tracker.isTracking, false);
      expect(tracker.isFinished, true);
      expect(tracker.startTime, startTime);
      expect(tracker.stopTime, time);
      expect(tracker.duration, const Duration(seconds: 5));
      additionalExpects();
    });

    test('Can track handled async exceptions', () async {
      await scopedTrack(() {
        time = time.add(const Duration(seconds: 1));
        return Future(() {
          time = time.add(const Duration(seconds: 2));
          throw 'error';
        }).then((_) {
          time = time.add(const Duration(seconds: 4));
        }).catchError((error, stack) {
          time = time.add(const Duration(seconds: 8));
        });
      });
      expect(tracker.isFinished, true);
      expect(tracker.startTime, startTime);
      expect(tracker.stopTime, time);
      expect(tracker.duration, const Duration(seconds: 11));
      additionalExpects();
    });

    test('Can track in case of unhandled async exceptions', () async {
      final future = scopedTrack(() {
        time = time.add(const Duration(seconds: 1));
        return Future(() {
          time = time.add(const Duration(seconds: 2));
          throw 'error';
        }).then((_) {
          time = time.add(const Duration(seconds: 4));
        });
      });
      await expectLater(future, throwsA(const TypeMatcher<String>()));
      expect(tracker.isFinished, true);
      expect(tracker.startTime, startTime);
      expect(tracker.stopTime, time);
      expect(tracker.duration, const Duration(seconds: 3));
      additionalExpects();
    });

    test('Can be nested async', () async {
      await scopedTrack(() async {
        time = time.add(const Duration(milliseconds: 1));
        await Future.value();
        time = time.add(const Duration(milliseconds: 2));
        await nestedTracker.track(() async {
          time = time.add(const Duration(milliseconds: 4));
          await Future.value();
          time = time.add(const Duration(milliseconds: 8));
          await Future.value();
          time = time.add(const Duration(milliseconds: 16));
        });
        time = time.add(const Duration(milliseconds: 32));
        await Future.value();
        time = time.add(const Duration(milliseconds: 64));
      });
      expect(tracker.isFinished, true);
      expect(tracker.startTime, startTime);
      expect(tracker.stopTime, time);
      expect(tracker.duration, const Duration(milliseconds: 127));
      expect(nestedTracker.startTime.isAfter(startTime), true);
      expect(nestedTracker.stopTime.isBefore(time), true);
      expect(nestedTracker.duration, const Duration(milliseconds: 28));
      additionalExpects();
    });
  }

  group('SyncTimeTracker', () {
    setUp(() {
      tracker = SyncTimeTracker();
      nestedTracker = SyncTimeTracker();
    });

    canHandleSync();

    test('Can not track async code', () async {
      await scopedTrack(() => Future(() {
            time = time.add(const Duration(seconds: 5));
          }));
      expect(tracker.isFinished, true);
      expect(tracker.startTime, startTime);
      expect(tracker.stopTime, startTime);
      expect(tracker.duration, const Duration(seconds: 0));
    });
  });

  group('AsyncTimeTracker.simple', () {
    setUp(() {
      tracker = SimpleAsyncTimeTracker();
      nestedTracker = SimpleAsyncTimeTracker();
    });

    canHandleSync();

    canHandleAsync();

    test('Can not distinguish own async code', () async {
      final future = scopedTrack(() => Future(() {
            time = time.add(const Duration(seconds: 5));
          }));
      time = time.add(const Duration(seconds: 10));
      await future;
      expect(tracker.isFinished, true);
      expect(tracker.startTime, startTime);
      expect(tracker.stopTime, time);
      expect(tracker.duration, const Duration(seconds: 15));
    });
  });

  group('AsyncTimeTracker', () {
    late AsyncTimeTracker asyncTracker;
    late AsyncTimeTracker nestedAsyncTracker;
    setUp(() {
      tracker = asyncTracker = AsyncTimeTracker();
      nestedTracker = nestedAsyncTracker = AsyncTimeTracker();
    });

    canHandleSync(() {
      expect(asyncTracker.innerDuration, asyncTracker.duration);
      expect(asyncTracker.slices.length, 1);
    });

    canHandleAsync(() {
      expect(asyncTracker.innerDuration, asyncTracker.duration);
      expect(asyncTracker.slices.length, greaterThan(1));
    });

    test('Can track complex async innerDuration', () async {
      final completer = Completer();
      final future = scopedTrack(() async {
        time = time.add(const Duration(seconds: 1)); // Tracked sync
        await Future.value();
        time = time.add(const Duration(seconds: 2)); // Tracked async
        await completer.future;
        time = time.add(const Duration(seconds: 4)); // Tracked async, delayed
      }).then((_) {
        time = time.add(const Duration(seconds: 8)); // Async, after tracking
      });
      time = time.add(const Duration(seconds: 16)); // Sync, between slices

      await Future(() {
        // Async, between slices
        time = time.add(const Duration(seconds: 32));
        completer.complete();
      });
      await future;
      expect(asyncTracker.isFinished, true);
      expect(asyncTracker.startTime, startTime);
      expect(asyncTracker.stopTime.isBefore(time), true);
      expect(asyncTracker.duration, const Duration(seconds: 55));
      expect(asyncTracker.innerDuration, const Duration(seconds: 7));
      expect(asyncTracker.slices.length, greaterThan(1));
    });

    test('Can exclude nested sync', () {
      tracker = asyncTracker = AsyncTimeTracker(trackNested: false);
      scopedTrack(() {
        time = time.add(const Duration(seconds: 1));
        nestedAsyncTracker.track(() {
          time = time.add(const Duration(seconds: 2));
        });
        time = time.add(const Duration(seconds: 4));
      });
      expect(asyncTracker.isFinished, true);
      expect(asyncTracker.startTime, startTime);
      expect(asyncTracker.stopTime, time);
      expect(asyncTracker.duration, const Duration(seconds: 7));
      expect(asyncTracker.innerDuration, const Duration(seconds: 5));
      expect(asyncTracker.slices.length, greaterThan(1));
      expect(nestedAsyncTracker.startTime.isAfter(startTime), true);
      expect(nestedAsyncTracker.stopTime.isBefore(time), true);
      expect(nestedAsyncTracker.duration, const Duration(seconds: 2));
      expect(nestedAsyncTracker.innerDuration, const Duration(seconds: 2));
      expect(nestedAsyncTracker.slices.length, 1);
    });

    test('Can exclude complex nested sync', () {
      tracker = asyncTracker = AsyncTimeTracker(trackNested: false);
      nestedAsyncTracker = AsyncTimeTracker(trackNested: false);
      final nestedAsyncTracker2 = AsyncTimeTracker(trackNested: false);
      scopedTrack(() {
        time = time.add(const Duration(seconds: 1));
        nestedAsyncTracker.track(() {
          time = time.add(const Duration(seconds: 2));
          nestedAsyncTracker2.track(() {
            time = time.add(const Duration(seconds: 4));
          });
          time = time.add(const Duration(seconds: 8));
        });
        time = time.add(const Duration(seconds: 16));
      });
      expect(asyncTracker.isFinished, true);
      expect(asyncTracker.startTime, startTime);
      expect(asyncTracker.stopTime, time);
      expect(asyncTracker.duration, const Duration(seconds: 31));
      expect(asyncTracker.innerDuration, const Duration(seconds: 17));
      expect(asyncTracker.slices.length, greaterThan(1));
      expect(nestedAsyncTracker.startTime.isAfter(startTime), true);
      expect(nestedAsyncTracker.stopTime.isBefore(time), true);
      expect(nestedAsyncTracker.duration, const Duration(seconds: 14));
      expect(nestedAsyncTracker.innerDuration, const Duration(seconds: 10));
      expect(nestedAsyncTracker.slices.length, greaterThan(1));
      expect(nestedAsyncTracker2.startTime.isAfter(startTime), true);
      expect(nestedAsyncTracker2.stopTime.isBefore(time), true);
      expect(nestedAsyncTracker2.duration, const Duration(seconds: 4));
      expect(nestedAsyncTracker2.innerDuration, const Duration(seconds: 4));
      expect(nestedAsyncTracker2.slices.length, 1);
    });

    test(
        'Can track all on grand-parent level and '
        'exclude grand-childrens from parent', () {
      tracker = asyncTracker = AsyncTimeTracker(trackNested: true);
      nestedAsyncTracker = AsyncTimeTracker(trackNested: false);
      final nestedAsyncTracker2 = AsyncTimeTracker();
      scopedTrack(() {
        time = time.add(const Duration(seconds: 1));
        nestedAsyncTracker.track(() {
          time = time.add(const Duration(seconds: 2));
          nestedAsyncTracker2.track(() {
            time = time.add(const Duration(seconds: 4));
          });
          time = time.add(const Duration(seconds: 8));
        });
        time = time.add(const Duration(seconds: 16));
      });
      expect(asyncTracker.isFinished, true);
      expect(asyncTracker.startTime, startTime);
      expect(asyncTracker.stopTime, time);
      expect(asyncTracker.duration, const Duration(seconds: 31));
      expect(asyncTracker.innerDuration, const Duration(seconds: 31));
      expect(asyncTracker.slices.length, 1);
      expect(nestedAsyncTracker.startTime.isAfter(startTime), true);
      expect(nestedAsyncTracker.stopTime.isBefore(time), true);
      expect(nestedAsyncTracker.duration, const Duration(seconds: 14));
      expect(nestedAsyncTracker.innerDuration, const Duration(seconds: 10));
      expect(nestedAsyncTracker.slices.length, greaterThan(1));
      expect(nestedAsyncTracker2.startTime.isAfter(startTime), true);
      expect(nestedAsyncTracker2.stopTime.isBefore(time), true);
      expect(nestedAsyncTracker2.duration, const Duration(seconds: 4));
      expect(nestedAsyncTracker2.innerDuration, const Duration(seconds: 4));
      expect(nestedAsyncTracker2.slices.length, 1);
    });

    test('Can exclude nested async', () async {
      tracker = asyncTracker = AsyncTimeTracker(trackNested: false);
      await scopedTrack(() async {
        time = time.add(const Duration(seconds: 1));
        await nestedAsyncTracker.track(() async {
          time = time.add(const Duration(seconds: 2));
          await Future.value();
          time = time.add(const Duration(seconds: 4));
          await Future.value();
          time = time.add(const Duration(seconds: 8));
        });
        time = time.add(const Duration(seconds: 16));
      });
      expect(asyncTracker.isFinished, true);
      expect(asyncTracker.startTime, startTime);
      expect(asyncTracker.stopTime, time);
      expect(asyncTracker.duration, const Duration(seconds: 31));
      expect(asyncTracker.innerDuration, const Duration(seconds: 17));
      expect(asyncTracker.slices.length, greaterThan(1));
      expect(nestedAsyncTracker.startTime.isAfter(startTime), true);
      expect(nestedAsyncTracker.stopTime.isBefore(time), true);
      expect(nestedAsyncTracker.duration, const Duration(seconds: 14));
      expect(nestedAsyncTracker.innerDuration, const Duration(seconds: 14));
      expect(nestedAsyncTracker.slices.length, greaterThan(1));
    });

    test('Can handle callbacks in excluded nested async', () async {
      tracker = asyncTracker = AsyncTimeTracker(trackNested: false);
      await scopedTrack(() async {
        time = time.add(const Duration(seconds: 1));
        final completer = Completer();
        final future = completer.future.then((_) {
          time = time.add(const Duration(seconds: 2));
        });
        await nestedAsyncTracker.track(() async {
          time = time.add(const Duration(seconds: 4));
          await Future.value();
          time = time.add(const Duration(seconds: 8));
          completer.complete();
          await future;
          time = time.add(const Duration(seconds: 16));
        });
        time = time.add(const Duration(seconds: 32));
      });
      expect(asyncTracker.isFinished, true);
      expect(asyncTracker.startTime, startTime);
      expect(asyncTracker.stopTime, time);
      expect(asyncTracker.duration, const Duration(seconds: 63));
      expect(asyncTracker.innerDuration, const Duration(seconds: 35));
      expect(asyncTracker.slices.length, greaterThan(1));
      expect(nestedAsyncTracker.startTime.isAfter(startTime), true);
      expect(nestedAsyncTracker.stopTime.isBefore(time), true);
      expect(nestedAsyncTracker.duration, const Duration(seconds: 30));
      expect(nestedAsyncTracker.innerDuration, const Duration(seconds: 28));
      expect(nestedAsyncTracker.slices.length, greaterThan(1));
    });
  });
}
