import 'dart:async';
import 'package:mutex/mutex.dart';
import 'package:test/test.dart';

//################################################################

class RWTester {
  int _operation = 0;
  final _operationSequences = <int>[];

  /// Execution sequence of the operations done.
  ///
  /// Each element corresponds to the position of the initial execution
  /// order of the read/write operation future.
  List<int> get operationSequences => _operationSequences;

  ReadWriteMutex mutex = ReadWriteMutex();

  /// Set to true to print out read/write to the balance during deposits
  static const bool debugOutput = false;

  final DateTime _startTime = DateTime.now();

  void _debugPrint(String message) {
    if (debugOutput) {
      final t = DateTime.now().difference(_startTime).inMilliseconds;
      // ignore: avoid_print
      print('$t: $message');
    }
  }

  void reset() {
    _operationSequences.clear();
    _debugPrint('reset');
  }

  /// Waits [startDelay] and then invokes critical section with mutex.
  ///
  /// Writes to [_operationSequences]. If the readwrite locks are respected
  /// then the final state of the list will be in ascending order.
  Future<void> writing(int startDelay, int sequence, int endDelay) async {
    await Future<void>.delayed(Duration(milliseconds: startDelay));

    await mutex.protectWrite(() async {
      final op = ++_operation;
      _debugPrint('[$op] write start: <- $_operationSequences');
      final tmp = _operationSequences;
      expect(mutex.isWriteLocked, isTrue);
      expect(_operationSequences, orderedEquals(tmp));
      // Add the position of operation to the list of operations.
      _operationSequences.add(sequence); // add position to list
      expect(mutex.isWriteLocked, isTrue);
      await Future<void>.delayed(Duration(milliseconds: endDelay));
      _debugPrint('[$op] write finish: -> $_operationSequences');
    });
  }

  /// Waits [startDelay] and then invokes critical section with mutex.
  ///
  ///
  Future<void> reading(int startDelay, int sequence, int endDelay) async {
    await Future<void>.delayed(Duration(milliseconds: startDelay));

    await mutex.protectRead(() async {
      final op = ++_operation;
      _debugPrint('[$op] read start: <- $_operationSequences');
      expect(mutex.isReadLocked, isTrue);
      _operationSequences.add(sequence); // add position to list
      await Future<void>.delayed(Duration(milliseconds: endDelay));
      _debugPrint('[$op] read finish: <- $_operationSequences');
    });
  }
}

//################################################################

//----------------------------------------------------------------

void main() {
  final account = RWTester();

  setUp(account.reset);

  test('multiple read locks', () async {
    await Future.wait([
      account.reading(0, 1, 1000),
      account.reading(0, 2, 900),
      account.reading(0, 3, 800),
      account.reading(0, 4, 700),
      account.reading(0, 5, 600),
      account.reading(0, 6, 500),
      account.reading(0, 7, 400),
      account.reading(0, 8, 300),
      account.reading(0, 9, 200),
      account.reading(0, 10, 100),
    ]);
    // The first future acquires the lock first and waits the longest to give it
    // up. This should however not block any of the other read operations
    // as such the reads should finish in ascending orders.
    expect(
      account.operationSequences,
      orderedEquals(<int>[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]),
    );
  });

  test('multiple write locks', () async {
    await Future.wait([
      account.writing(0, 1, 100),
      account.writing(0, 2, 100),
      account.writing(0, 3, 100),
    ]);
    // The first future writes first and holds the lock until 100 ms
    // Even though the second future starts execution, the lock cannot be
    // acquired until it is released by the first future.
    // Therefore the sequence of operations will be in ascending order
    // of the futures.
    expect(
      account.operationSequences,
      orderedEquals(<int>[1, 2, 3]),
    );
  });

  test('acquireWrite() before acquireRead()', () async {
    const lockTimeout = Duration(milliseconds: 100);

    final mutex = ReadWriteMutex();

    await mutex.acquireWrite();
    expect(mutex.isReadLocked, equals(false));
    expect(mutex.isWriteLocked, equals(true));

    // Since there is a write lock existing, a read lock cannot be acquired.
    final readLock = mutex.acquireRead().timeout(lockTimeout);
    expect(
      () async => readLock,
      throwsA(isA<TimeoutException>()),
    );
  });

  test('acquireRead() before acquireWrite()', () async {
    const lockTimeout = Duration(milliseconds: 100);

    final mutex = ReadWriteMutex();

    await mutex.acquireRead();
    expect(mutex.isReadLocked, equals(true));
    expect(mutex.isWriteLocked, equals(false));

    // Since there is a read lock existing, a write lock cannot be acquired.
    final writeLock = mutex.acquireWrite().timeout(lockTimeout);
    expect(
      () async => writeLock,
      throwsA(isA<TimeoutException>()),
    );
  });

  test('mixture of read write locks execution order', () async {
    await Future.wait([
      account.reading(0, 1, 100),
      account.reading(10, 2, 100),
      account.reading(20, 3, 100),
      account.writing(30, 4, 100),
      account.writing(40, 5, 100),
      account.writing(50, 6, 100),
    ]);

    expect(
      account.operationSequences,
      orderedEquals(<int>[1, 2, 3, 4, 5, 6]),
    );
  });

  group('protectRead', () {
    test('lock obtained and released on success', () async {
      final m = ReadWriteMutex();

      await m.protectRead(() async {
        // critical section
        expect(m.isLocked, isTrue);
      });
      expect(m.isLocked, isFalse);
    });

    test('value returned from critical section', () async {
      // These are the normal scenario of the critical section running
      // successfully. It tests different return types from the
      // critical section.

      final m = ReadWriteMutex();

      // returns Future<void>
      await m.protectRead<void>(() async {});

      // returns Future<int>
      final number = await m.protectRead<int>(() async => 42);
      expect(number, equals(42));

      // returns Future<int?> completes with value
      final optionalNumber = await m.protectRead<int?>(() async => 1024);
      expect(optionalNumber, equals(1024));

      // returns Future<int?> completes with null
      final optionalNumberNull = await m.protectRead<int?>(() async => null);
      expect(optionalNumberNull, isNull);

      // returns Future<String>
      final word = await m.protectRead<String>(() async => 'foobar');
      expect(word, equals('foobar'));

      // returns Future<String?> completes with value
      final optionalWord = await m.protectRead<String?>(() async => 'baz');
      expect(optionalWord, equals('baz'));

      // returns Future<String?> completes with null
      final optionalWordNull = await m.protectRead<String?>(() async => null);
      expect(optionalWordNull, isNull);

      expect(m.isLocked, isFalse);
    });

    test('exception in synchronous code', () async {
      // Tests what happens when an exception is raised in the **synchronous**
      // part of the critical section.
      //
      // Locks are correctly managed: the lock is obtained before executing
      // the critical section, and is released when the exception is thrown
      // by the _protect_ method.
      //
      // The exception is raised when waiting for the Future returned by
      // _protect_ to complete. Even though the exception is synchronously
      // raised by the critical section, it won't be thrown when _protect_
      // is invoked. The _protect_ method always successfully returns a
      // _Future_.

      Future<int> criticalSection() {
        final c = Completer<int>()..complete(42);

        // synchronous exception
        throw const FormatException('synchronous exception');
        // ignore: dead_code
        return c.future;
      }

      // Check the criticalSection behaves as expected for the test

      try {
        // ignore: unused_local_variable
        final resultFuture = criticalSection();
        fail('critical section did not throw synchronous exception');
      } on FormatException {
        // expected: invoking the criticalSection results in the exception
      }

      final m = ReadWriteMutex();

      try {
        // Invoke protect to get the Future (this should succeed)
        final resultFuture = m.protectRead<int>(criticalSection);
        expect(resultFuture, isA<Future<int>>());

        // Wait for the Future (this should fail)
        final result = await resultFuture;
        expect(result, isNotNull);
        fail('exception not thrown');
      } on FormatException catch (e) {
        expect(m.isLocked, isFalse);
        expect(e.message, equals('synchronous exception'));
      }

      expect(m.isLocked, isFalse);
    });

    test('exception in asynchronous code', () async {
      // Tests what happens when an exception is raised in the **asynchronous**
      // part of the critical section.
      //
      // Locks are correctly managed: the lock is obtained before executing
      // the critical section, and is released when the exception is thrown
      // by the _protect_ method.
      //
      // The exception is raised when waiting for the Future returned by
      // _protect_ to complete.

      Future<int> criticalSection() async {
        final c = Completer<int>()..complete(42);

        await Future.delayed(const Duration(seconds: 1), () {});

        // asynchronous exception (since it must wait for the above line)
        throw const FormatException('asynchronous exception');
        // ignore: dead_code
        return c.future;
      }

      // Check the criticalSection behaves as expected for the test

      final resultFuture = criticalSection();
      expect(resultFuture, isA<Future<int>>());
      // invoking the criticalSection does not result in the exception
      try {
        await resultFuture;
        fail('critical section did not throw asynchronous exception');
      } on FormatException {
        // expected: exception happens on the await
      }

      final m = ReadWriteMutex();

      try {
        // Invoke protect to get the Future (this should succeed)
        final resultFuture = m.protectRead<int>(criticalSection);
        expect(resultFuture, isA<Future<int>>());

        // Even though the criticalSection throws the exception in synchronous
        // code, protect causes it to become an asynchronous exception.

        // Wait for the Future (this should fail)
        final result = await resultFuture;
        expect(result, isNotNull);
        fail('exception not thrown');
      } on FormatException catch (e) {
        expect(m.isLocked, isFalse);
        expect(e.message, equals('asynchronous exception'));
      }

      expect(m.isLocked, isFalse);
    });
  });

  group('protectWrite', () {
    test('lock obtained and released on success', () async {
      final m = ReadWriteMutex();

      await m.protectWrite(() async {
        // critical section
        expect(m.isLocked, isTrue);
      });
      expect(m.isLocked, isFalse);
    });

    test('value returned from critical section', () async {
      // These are the normal scenario of the critical section running
      // successfully. It tests different return types from the
      // critical section.

      final m = ReadWriteMutex();

      // returns Future<void>
      await m.protectWrite<void>(() async {});

      // returns Future<int>
      final number = await m.protectWrite<int>(() async => 42);
      expect(number, equals(42));

      // returns Future<int?> completes with value
      final optionalNumber = await m.protectWrite<int?>(() async => 1024);
      expect(optionalNumber, equals(1024));

      // returns Future<int?> completes with null
      final optionalNumberNull = await m.protectWrite<int?>(() async => null);
      expect(optionalNumberNull, isNull);

      // returns Future<String>
      final word = await m.protectWrite<String>(() async => 'foobar');
      expect(word, equals('foobar'));

      // returns Future<String?> completes with value
      final optionalWord = await m.protectWrite<String?>(() async => 'baz');
      expect(optionalWord, equals('baz'));

      // returns Future<String?> completes with null
      final optionalWordNull = await m.protectWrite<String?>(() async => null);
      expect(optionalWordNull, isNull);

      expect(m.isLocked, isFalse);
    });

    test('exception in synchronous code', () async {
      // Tests what happens when an exception is raised in the **synchronous**
      // part of the critical section.
      //
      // Locks are correctly managed: the lock is obtained before executing
      // the critical section, and is released when the exception is thrown
      // by the _protect_ method.
      //
      // The exception is raised when waiting for the Future returned by
      // _protect_ to complete. Even though the exception is synchronously
      // raised by the critical section, it won't be thrown when _protect_
      // is invoked. The _protect_ method always successfully returns a
      // _Future_.

      Future<int> criticalSection() {
        final c = Completer<int>()..complete(42);

        // synchronous exception
        throw const FormatException('synchronous exception');
        // ignore: dead_code
        return c.future;
      }

      // Check the criticalSection behaves as expected for the test

      try {
        // ignore: unused_local_variable
        final resultFuture = criticalSection();
        fail('critical section did not throw synchronous exception');
      } on FormatException {
        // expected: invoking the criticalSection results in the exception
      }

      final m = ReadWriteMutex();

      try {
        // Invoke protect to get the Future (this should succeed)
        final resultFuture = m.protectWrite<int>(criticalSection);
        expect(resultFuture, isA<Future<int>>());

        // Wait for the Future (this should fail)
        final result = await resultFuture;
        expect(result, isNotNull);
        fail('exception not thrown');
      } on FormatException catch (e) {
        expect(m.isLocked, isFalse);
        expect(e.message, equals('synchronous exception'));
      }

      expect(m.isLocked, isFalse);
    });

    test('exception in asynchronous code', () async {
      // Tests what happens when an exception is raised in the **asynchronous**
      // part of the critical section.
      //
      // Locks are correctly managed: the lock is obtained before executing
      // the critical section, and is released when the exception is thrown
      // by the _protect_ method.
      //
      // The exception is raised when waiting for the Future returned by
      // _protect_ to complete.

      Future<int> criticalSection() async {
        final c = Completer<int>()..complete(42);

        await Future.delayed(const Duration(seconds: 1), () {});

        // asynchronous exception (since it must wait for the above line)
        throw const FormatException('asynchronous exception');
        // ignore: dead_code
        return c.future;
      }

      // Check the criticalSection behaves as expected for the test

      final resultFuture = criticalSection();
      expect(resultFuture, isA<Future<int>>());
      // invoking the criticalSection does not result in the exception
      try {
        await resultFuture;
        fail('critical section did not throw asynchronous exception');
      } on FormatException {
        // expected: exception happens on the await
      }

      final m = ReadWriteMutex();

      try {
        // Invoke protect to get the Future (this should succeed)
        final resultFuture = m.protectWrite<int>(criticalSection);
        expect(resultFuture, isA<Future<int>>());

        // Even though the criticalSection throws the exception in synchronous
        // code, protect causes it to become an asynchronous exception.

        // Wait for the Future (this should fail)
        final result = await resultFuture;
        expect(result, isNotNull);
        fail('exception not thrown');
      } on FormatException catch (e) {
        expect(m.isLocked, isFalse);
        expect(e.message, equals('asynchronous exception'));
      }

      expect(m.isLocked, isFalse);
    });
  });
}
