public async Task UpdateShouldSetAndResetLoadingFlagOfAffectedOptions()
        {
            // Arrange
            var tcs               = new TaskCompletionSource <int>();
            var dispatcher        = CreateDispatcher();
            var updateFinishBlock = new ManualResetEventSlim(false);

            async Task <IValueCalculationResult <int> > CalculationTaskFactory(CancellationToken ct)
            {
                await Task.Delay(50, ct);

                return(ValueCalculationResult.Success(123));
            }

            void HandleResultCallback(int arg)
            {
                tcs.SetResult(arg);
            }

            using var sut = new AsyncValueCalculator <int>(dispatcher, CalculationTaskFactory, HandleResultCallback);

            var option = Substitute.For <IValueOption>();

            sut.Affect(option);

            option.When(o => o.IsLoading = false)
            .Do(x => updateFinishBlock.Set());

            // Act
            sut.Update();

            // Assert
            await tcs.Task;

            updateFinishBlock.Wait(TimeSpan.FromSeconds(1));

            Assert.False(option.IsLoading);

            option.Received(1).IsLoading = true;
            option.Received(1).IsLoading = false;
        }
        public async Task UpdateShouldNotThrowWhenTaskThrows()
        {
            // Arrange
            var tcs        = new TaskCompletionSource <int>();
            var dispatcher = CreateDispatcher();

            Task <IValueCalculationResult <int> > CalculationTaskFactory(CancellationToken ct)
            {
                Task.Run(() =>
                {
                    Thread.Sleep(15);
                    tcs.SetResult(0);
                }, ct);

                return(Task.FromException <IValueCalculationResult <int> >(new Exception("Test")));
            }

            void HandleResultCallback(int arg)
            {
                tcs.SetResult(arg);
            }

            using var sut = new AsyncValueCalculator <int>(dispatcher, CalculationTaskFactory, HandleResultCallback);

            var option = Substitute.For <IValueOption>();

            sut.Affect(option);

            // Act
            sut.Update();

            // Assert
            var result = await tcs.Task;

            option.Received(1).IsLoading = true;
            option.Received(2).IsLoading = false;

            Assert.Equal(0, result);
        }
        public async Task MultipleUpdateShouldTriggerCallbackOnlyOnce()
        {
            // As with all async code there is much magic happening here:
            // We want to start a second update while the first one is still running.
            // To do so we start the first update wait for a WaitHandle to be
            // signaled and then start the second update.
            //
            // When the update method is run, it will signal the wait handle
            // then wait some time to let the second update start
            //
            // The callback will set a TaskCompletionSource and this task is awaited
            // in the test.
            //
            // The asserts check that
            // - The update method was called twice
            // - The result callback was only called once
            // - The affected option is not loading anymore
            // - The loading flags was set and unset correctly
            //

            // Arrange
            var tcs              = new TaskCompletionSource <int>();
            var dispatcher       = CreateDispatcher();
            var callbackCount    = 0;
            var calculationCount = 0;

            var updateStartBlock  = new ManualResetEventSlim(false);
            var updateFinishBlock = new ManualResetEventSlim(false);

            async Task <IValueCalculationResult <int> > CalculationTaskFactory(CancellationToken ct)
            {
                Interlocked.Increment(ref calculationCount);
                updateStartBlock.Set();
                await Task.Delay(25, ct);

                return(ValueCalculationResult.Success(123));
            }

            void HandleResultCallback(int arg)
            {
                Interlocked.Increment(ref callbackCount);
                tcs.SetResult(arg);
            }

            using var sut = new AsyncValueCalculator <int>(dispatcher, CalculationTaskFactory, HandleResultCallback);

            var option = Substitute.For <IValueOption>();

            sut.Affect(option);

            option.When(o => o.IsLoading = false)
            .Do(x => updateFinishBlock.Set());

            // Act
            sut.Update();
            updateStartBlock.Wait();
            sut.Update();

            // Assert
            await tcs.Task;

            updateFinishBlock.Wait(TimeSpan.FromSeconds(1));

            Assert.Equal(1, callbackCount);
            Assert.Equal(2, calculationCount);

            Assert.False(option.IsLoading);

            option.Received(2).IsLoading = true;
            option.Received(1).IsLoading = false;
        }