public async Task ShouldNotLoseAudioSamplesInCaseIfExceptionIsThrown()
        {
            var audioService = new SoundFingerprintingAudioService();
            var modelService = new InMemoryModelService();

            int count = 10, found = 0, didNotPassThreshold = 0, thresholdVotes = 4, testWaitTime = 40000, fingerprintsCount = 0, errored = 0;
            var data = GenerateRandomAudioChunks(count, 1);
            var concatenated = Concatenate(data);
            var hashes = await FingerprintCommandBuilder.Instance
                                                .BuildFingerprintCommand()
                                                .From(concatenated)
                                                .UsingServices(audioService)
                                                .Hash();

            modelService.Insert(new TrackInfo("312", "Bohemian Rhapsody", "Queen"), hashes);

            var started = DateTime.Now;
            var resultEntries = new List<ResultEntry>();
            var collection = SimulateRealtimeQueryData(data, true, TimeSpan.FromSeconds);

            var cancellationTokenSource = new CancellationTokenSource(testWaitTime);
            var offlineStorage = new OfflineStorage(Path.GetTempPath());
            var restoreCalled = new bool[1];
            double processed = await new RealtimeQueryCommand(FingerprintCommandBuilder.Instance, new FaultyQueryService(count, QueryFingerprintService.Instance))
                 .From(collection)
                 .WithRealtimeQueryConfig(config =>
                 {
                     config.SuccessCallback = entry =>
                     {
                         Interlocked.Increment(ref found);
                         resultEntries.Add(entry);
                     };

                     config.QueryFingerprintsCallback = fingerprints => Interlocked.Increment(ref fingerprintsCount);
                     config.DidNotPassFilterCallback = entry => Interlocked.Increment(ref didNotPassThreshold);
                     config.ErrorCallback = (exception, timedHashes) =>
                     {
                         Interlocked.Increment(ref errored);
                         offlineStorage.Save(timedHashes);
                     };
                     
                     config.ResultEntryFilter = new QueryMatchLengthFilter(10);
                     config.RestoredAfterErrorCallback = () => restoreCalled[0] = true;
                     config.PermittedGap = 1.48d;
                     config.ThresholdVotes = thresholdVotes;
                     config.DowntimeHashes = offlineStorage;
                     config.DowntimeCapturePeriod = 3d;
                     return config;
                 })
                 .UsingServices(modelService)
                 .Query(cancellationTokenSource.Token);

            Assert.AreEqual(count, errored);
            Assert.AreEqual(20, fingerprintsCount);
            Assert.IsTrue(restoreCalled[0]);
            Assert.AreEqual(1, found);
            var resultEntry = resultEntries[0];
            double jitterLength = 5 * 10240 / 5512d;
            Assert.AreEqual(0d, started.AddSeconds(jitterLength + resultEntry.TrackMatchStartsAt).Subtract(resultEntry.MatchedAt).TotalSeconds, 2d);
            Assert.AreEqual(1, didNotPassThreshold);
            Assert.AreEqual((count + 10) * 10240 / 5512d, processed, 0.2);
        }
        public async Task ShouldNotLoseAudioSamplesInCaseIfExceptionIsThrown()
        {
            var audioService = new SoundFingerprintingAudioService();
            var modelService = new InMemoryModelService();

            const double minSizeChunk     = 10240d / 5512; // length in seconds of one query chunk ~1.8577
            const double totalTrackLength = 210;           // length of the track 3 minutes 30 seconds.
            const double jitterLength     = 10;
            double       totalQueryLength = totalTrackLength + 2 * jitterLength;

            int trackCount = (int)(totalTrackLength / minSizeChunk), fingerprintsCount = 0, errored = 0, didNotPassThreshold = 0, jitterChunks = 2;

            var start        = new DateTime(2021, 5, 1, 0, 0, 0, DateTimeKind.Utc);
            var data         = GenerateRandomAudioChunks(trackCount, seed: 1, start);
            var concatenated = Concatenate(data);
            var hashes       = await FingerprintCommandBuilder.Instance
                               .BuildFingerprintCommand()
                               .From(concatenated)
                               .UsingServices(audioService)
                               .Hash();

            modelService.Insert(new TrackInfo("312", "Bohemian Rhapsody", "Queen"), hashes);

            var resultEntries = new List <ResultEntry>();
            var collection    = SimulateRealtimeQueryData(data, jitterLength);

            var    offlineStorage = new OfflineStorage(Path.GetTempPath());
            var    restoreCalled  = new bool[1];
            double processed      = await new RealtimeQueryCommand(FingerprintCommandBuilder.Instance, new FaultyQueryService(faultyCounts: trackCount + jitterChunks - 1, QueryFingerprintService.Instance))
                                    .From(collection)
                                    .WithRealtimeQueryConfig(config =>
            {
                config.SuccessCallback = entry =>
                {
                    resultEntries.Add(entry);
                };

                config.DidNotPassFilterCallback = _ => Interlocked.Increment(ref didNotPassThreshold);
                config.ErrorCallback            = (_, timedHashes) =>
                {
                    Interlocked.Increment(ref errored);
                    offlineStorage.Save(timedHashes);
                };

                config.ResultEntryFilter          = new TrackRelativeCoverageLengthEntryFilter(0.4, waitTillCompletion: true);
                config.RestoredAfterErrorCallback = () => restoreCalled[0] = true;
                config.DowntimeHashes             = offlineStorage;       // store the other half of the fingerprints in the downtime hashes storage
                config.DowntimeCapturePeriod      = totalQueryLength / 2; // store half of the fingerprints in the offline storage
                return(config);
            })
                                    .Intercept(fingerprints =>
            {
                Interlocked.Increment(ref fingerprintsCount);
                return(fingerprints);
            })
                                    .UsingServices(modelService)
                                    .Query(CancellationToken.None);

            Assert.AreEqual(totalQueryLength, processed, 1);
            Assert.AreEqual(trackCount + jitterChunks - 1, errored);
            Assert.AreEqual(trackCount, fingerprintsCount - jitterChunks);
            Assert.IsTrue(restoreCalled[0]);
            Assert.AreEqual(0, didNotPassThreshold);
            Assert.AreEqual(1, resultEntries.Count);
            var result = resultEntries.First();

            Assert.AreEqual(totalTrackLength, result.Coverage.TrackCoverageWithPermittedGapsLength, 1);
            Assert.IsTrue(Math.Abs(start.Subtract(result.MatchedAt).TotalSeconds) < 2, $"Matched At {result.MatchedAt:o}");
        }