From 3687be816be88502e8647456daede4eb3bf8782c Mon Sep 17 00:00:00 2001
From: AlexanderAlferov <64849422+AlexanderAlferov@users.noreply.github.com>
Date: Thu, 12 Nov 2020 18:27:10 +0300
Subject: [PATCH] Parse test JSON files and init unit tests with respective
 parameters (EXPOSUREAPP-3456) (#1575)

* Split and hide the protobuf config behind interfaces with individual mappers responsible for creating the desired formats.

* Merge branch 'release/1.7.x' into feature/3455-more-frequent-riskscore-updates-configs

# Conflicts:
#	Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/risklevel/ui/TestRiskLevelCalculationFragmentCWAViewModel.kt
#	Corona-Warn-App/src/main/java/de/rki/coronawarnapp/transaction/RiskLevelTransaction.kt
#	Corona-Warn-App/src/test/java/de/rki/coronawarnapp/transaction/RiskLevelTransactionTest.kt

* Make the AppConfig observable.
Provide the server time offset.
Offer a lastUpdatedAt timestamp.
Add an app config specific test screen.
Clean up test screens a bit and move debug options out of API test options.

* Fix test regression due to refactoring (moved code around).

* Store the server timestamp and offset at retrieval.
Switch to config storage via json to be able to store additional meta data fields (i.e. time).

* KLint and Me have a hate relationship based on both mutual admiration.

* Fix time offset parsing being locale dependent.

* Fix broken unit tests.

* Improve offset accuracy, move before unzipping.

* Fix overly long livedata subscription to results (viewmodel scope vs observer scope)

* Add mapping for the new protobuf configs + tests.

* For cached (retrofit) response, we need to check the cacheResponse and its timestamps
to determine an accurate time offset.

* Exposure a boolean property to tell us when a fallback config is being used.

* Hide the observable flow<ConfigData> behind a method that can automatically triggers refreshes.

* Use a common mapper interface.

* set old risklevelcalculation deprecated

* Created skeleton for new risk calculation and aggregation

* Initial

* Implementing steps to aggregate results form exposure windows - wip

* Address PR comments and KLints.

* Fix refactoring regression.

* ktlint

* Json parsing

* Added ExposureWindowRiskLevelConfig and ExposureWindowRiskLevelConfigMapper for new config api (not yet introduced)

Signed-off-by: Kolya Opahle <k.opahle@sap.com>

* Added first Implementation of exposure window based calculateRisk function

Signed-off-by: Kolya Opahle <k.opahle@sap.com>

* Added generics to Range.inRange

Signed-off-by: Kolya Opahle <k.opahle@sap.com>

* Added Ugly Hack to RiskLevelTransaction to allow for compilation during testing

Signed-off-by: Kolya Opahle <k.opahle@sap.com>

* Linting and injecting RiskLevelCalculation into TestRiskLevelCalculationFragmentCWAViewModel, currently wont build because ExposureWindowRiskLevelConfig has no Provider

Signed-off-by: Kolya Opahle <k.opahle@sap.com>

* Linting extravaganza

Signed-off-by: Kolya Opahle <k.opahle@sap.com>

* Lint Wars Episode VI: Return of the trailing Comma

* Improve config unzipping code.

* Add flag to forward exception thrown during HotDataFlow.kt initialization.

* Don't specify a default context via singleton.

* Move download and fallback logic into it's own class just responsible for sourcing the config: "AppConfigSource".
"AppConfigProvider" is now only responsible for making it available.

* Check test cases

* Simplify current concepts for making the app config observable until we have a default configuration.

* Implementing steps to aggregate results form exposure windows

* cleaned todo

* Adjusted default values

* Improve app config test screen, delete options, better feedback.
Show toast instead of crash on errors.

* Fixed GSON serialization not encoding/decoding the byte array correctly.
Added specific type adapters for instant and duration to get cleaner json.

* Remove type adapters from base gson due to conflict with CalculationTrackerStorage.

* refactored Windows aggregation

* We want to default to forced serialization of instant by our converters, instead of using the default serialization which will differ
between Java8.Instant and JodaTime.Instant, to prevent future headaches there, register explicit converters by default,
and overwrite them if necessary (currently only needed for CalculationTrackerStorage.kt).

* Improve AppConfigServer code readability by moving code into extensions.

* Fix merge conflicts

* removed example value

* Added missing import to WorkerBinderTest

* fixed unit tests

* Removed auto formatting on unrelated files (revert + cherry pick in other commit)

Signed-off-by: Kolya Opahle <k.opahle@sap.com>

* Implementing steps to aggregate results form exposure windows

* Renamed ExposureWindowRiskLevelConfig to ExposureWindowRiskCalculationConfig

* adjusted & refactored Windows aggregation

* removed example Values

* satisfy lint

* make Aggregation work with Instant now

* Use long while calculation

* Added normalizedTimePerDayToRiskLevelMappingList to AppConfig

* normalizedTimePerDayToRiskLevelMappingList from AppConfig

* satisfy lint

* Get AppConfig on init and listen for updates

* exposureData to aggregatedRiskPerDateResult

* Corrected name in ConfigParserTest

* use instant for specific aggregation logs

* satisfy CI

* satisfy detekt

* Mock exposure windows

* Full test process

* Fix gitignore

* Improved logging

* Correct test cases dates handling

* Config fix and logs

* Small clean up

* fixed some naming and conversion issues with json test case parsing

Signed-off-by: Kolya Opahle <k.opahle@sap.com>

* TRL Encodings in config did not match TRL Encodings used in js example

* Removing a return that broke the calculation

* Actual tests and formatting

* Formatting

Co-authored-by: Matthias Urhahn <matthias.urhahn@sap.com>
Co-authored-by: BMItter <Berndus@gmx.de>
Co-authored-by: Kolya Opahle <k.opahle@sap.com>
Co-authored-by: harambasicluka <64483219+harambasicluka@users.noreply.github.com>
---
 .../coronawarnapp/risk/DefaultRiskLevels.kt   |    2 +-
 .../windows/ExposureWindowsCalculationTest.kt |  402 +++++++
 .../entities/ExposureWindowsJsonInput.kt      |   14 +
 .../entities/cases/JsonScanInstance.kt        |   12 +
 .../windows/entities/cases/JsonWindow.kt      |   16 +
 .../nearby/windows/entities/cases/TestCase.kt |   20 +
 .../DefaultRiskCalculationConfiguration.kt    |   20 +
 .../JsonMinutesAtAttenuationFilter.kt         |   10 +
 .../JsonMinutesAtAttenuationWeight.kt         |   10 +
 .../JsonNormalizedTimeToRiskLevelMapping.kt   |   10 +
 .../entities/configuration/JsonTrlEncoding.kt |   18 +
 .../entities/configuration/JsonTrlFilter.kt   |    8 +
 .../windows/entities/configuration/Range.kt   |   14 +
 .../exposure-windows-risk-calculation.json    | 1025 +++++++++++++++++
 14 files changed, 1580 insertions(+), 1 deletion(-)
 create mode 100644 Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/ExposureWindowsCalculationTest.kt
 create mode 100644 Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/ExposureWindowsJsonInput.kt
 create mode 100644 Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/cases/JsonScanInstance.kt
 create mode 100644 Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/cases/JsonWindow.kt
 create mode 100644 Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/cases/TestCase.kt
 create mode 100644 Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/configuration/DefaultRiskCalculationConfiguration.kt
 create mode 100644 Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/configuration/JsonMinutesAtAttenuationFilter.kt
 create mode 100644 Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/configuration/JsonMinutesAtAttenuationWeight.kt
 create mode 100644 Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/configuration/JsonNormalizedTimeToRiskLevelMapping.kt
 create mode 100644 Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/configuration/JsonTrlEncoding.kt
 create mode 100644 Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/configuration/JsonTrlFilter.kt
 create mode 100644 Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/configuration/Range.kt
 create mode 100644 Corona-Warn-App/src/test/resources/exposure-windows-risk-calculation.json

diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/DefaultRiskLevels.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/DefaultRiskLevels.kt
index ada0d0cc0..146f92532 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/DefaultRiskLevels.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/DefaultRiskLevels.kt
@@ -308,7 +308,7 @@ class DefaultRiskLevels @Inject constructor(
                     .filter { it.attenuationRange.inRange(scanInstance.typicalAttenuationDb) }
                     .map { it.weight }
                     .firstOrNull() ?: .0
-            return seconds + scanInstance.secondsSinceLastScan * weight
+            seconds + scanInstance.secondsSinceLastScan * weight
         }
 
     private fun determineRiskLevel(
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/ExposureWindowsCalculationTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/ExposureWindowsCalculationTest.kt
new file mode 100644
index 000000000..3dab010d2
--- /dev/null
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/ExposureWindowsCalculationTest.kt
@@ -0,0 +1,402 @@
+package de.rki.coronawarnapp.nearby.windows
+
+import com.google.android.gms.nearby.exposurenotification.ExposureWindow
+import com.google.android.gms.nearby.exposurenotification.ScanInstance
+import com.google.gson.Gson
+import de.rki.coronawarnapp.appconfig.AppConfigProvider
+import de.rki.coronawarnapp.appconfig.ConfigData
+import de.rki.coronawarnapp.appconfig.DefaultConfigData
+import de.rki.coronawarnapp.nearby.windows.entities.ExposureWindowsJsonInput
+import de.rki.coronawarnapp.nearby.windows.entities.cases.JsonScanInstance
+import de.rki.coronawarnapp.nearby.windows.entities.cases.JsonWindow
+import de.rki.coronawarnapp.nearby.windows.entities.cases.TestCase
+import de.rki.coronawarnapp.nearby.windows.entities.configuration.DefaultRiskCalculationConfiguration
+import de.rki.coronawarnapp.nearby.windows.entities.configuration.JsonMinutesAtAttenuationFilter
+import de.rki.coronawarnapp.nearby.windows.entities.configuration.JsonMinutesAtAttenuationWeight
+import de.rki.coronawarnapp.nearby.windows.entities.configuration.JsonNormalizedTimeToRiskLevelMapping
+import de.rki.coronawarnapp.nearby.windows.entities.configuration.JsonTrlFilter
+import de.rki.coronawarnapp.risk.DefaultRiskLevels
+import de.rki.coronawarnapp.risk.result.AggregatedRiskResult
+import de.rki.coronawarnapp.risk.result.RiskResult
+import de.rki.coronawarnapp.server.protocols.internal.v2.RiskCalculationParametersOuterClass
+import de.rki.coronawarnapp.util.TimeStamper
+import de.rki.coronawarnapp.util.serialization.fromJson
+import io.kotest.matchers.ints.shouldBeGreaterThan
+import io.kotest.matchers.shouldBe
+import io.kotest.matchers.shouldNotBe
+import io.mockk.MockKAnnotations
+import io.mockk.clearAllMocks
+import io.mockk.coEvery
+import io.mockk.every
+import io.mockk.impl.annotations.MockK
+import io.mockk.mockk
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.runBlocking
+import org.joda.time.DateTimeConstants
+import org.joda.time.Duration
+import org.joda.time.Instant
+import org.junit.jupiter.api.AfterEach
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import testhelpers.BaseTest
+import timber.log.Timber
+import java.io.FileReader
+import java.nio.file.Paths
+
+class ExposureWindowsCalculationTest : BaseTest() {
+
+    @MockK lateinit var appConfigProvider: AppConfigProvider
+    @MockK lateinit var configData: ConfigData
+    @MockK lateinit var timeStamper: TimeStamper
+
+    private lateinit var riskLevels: DefaultRiskLevels
+    private lateinit var testConfig: ConfigData
+
+    // Json file (located in /test/resources/exposure-windows-risk-calculation.json)
+    private val fileName = "exposure-windows-risk-calculation.json"
+
+    // Debug logs
+    private enum class LogLevel {
+        NONE,
+        ONLY_COMPARISON,
+        EXTENDED,
+        ALL
+    }
+    private val logLevel = LogLevel.ONLY_COMPARISON
+
+    @BeforeEach
+    fun setup() {
+        MockKAnnotations.init(this)
+        every { timeStamper.nowUTC } returns Instant.now()
+    }
+
+    @AfterEach
+    fun teardown() {
+        clearAllMocks()
+    }
+
+    private fun debugLog(s: String, toShow: LogLevel = LogLevel.ALL) {
+        if (logLevel < toShow)
+            return
+        Timber.v(s)
+    }
+
+    @Test
+    fun `one test to rule them all`(): Unit = runBlocking {
+        // 1 - Load and parse json file
+        val jsonFile = Paths.get("src", "test", "resources", fileName).toFile()
+        jsonFile shouldNotBe null
+        val jsonString = FileReader(jsonFile).readText()
+        jsonString.length shouldBeGreaterThan 0
+        val json = Gson().fromJson<ExposureWindowsJsonInput>(jsonString)
+        json shouldNotBe null
+
+        // 2 - Check test cases
+        for (case: TestCase in json.testCases) {
+            checkTestCase(case)
+        }
+        debugLog("Test cases checked. Total count: ${json.testCases.size}")
+
+        // 3 - Mock calculation configuration and create default risk level with it
+        jsonToConfiguration(json.defaultRiskCalculationConfiguration)
+        coEvery { appConfigProvider.getAppConfig() } returns testConfig
+        every { appConfigProvider.currentConfig } returns flow { testConfig }
+        logConfiguration(testConfig)
+        riskLevels = DefaultRiskLevels(appConfigProvider)
+
+        // 4 - Mock and log exposure windows
+        val allExposureWindows = mutableListOf<ExposureWindow>()
+        for (case: TestCase in json.testCases) {
+            val exposureWindows: List<ExposureWindow> =
+                case.exposureWindows.map { window -> jsonToExposureWindow(window) }
+            allExposureWindows.addAll(exposureWindows)
+
+            // 5 - Calculate risk level for test case and aggregate results
+            val exposureWindowsAndResult = HashMap<ExposureWindow, RiskResult>()
+            for (exposureWindow: ExposureWindow in exposureWindows) {
+
+                logExposureWindow(exposureWindow, "âž¡âž¡ EXPOSURE WINDOW PASSED âž¡âž¡", LogLevel.EXTENDED)
+                val riskResult = riskLevels.calculateRisk(exposureWindow) ?: continue
+                exposureWindowsAndResult[exposureWindow] = riskResult
+            }
+            debugLog("Exposure windows and result: ${exposureWindowsAndResult.size}")
+
+            val aggregatedRiskResult = riskLevels.aggregateResults(exposureWindowsAndResult)
+
+            debugLog(
+                "\n" + comparisonDebugTable(aggregatedRiskResult, case),
+                LogLevel.ONLY_COMPARISON
+            )
+
+            // 6 - Check with expected result from test case
+            aggregatedRiskResult.totalRiskLevel.number shouldBe case.expTotalRiskLevel
+            aggregatedRiskResult.mostRecentDateWithHighRisk shouldBe getTestCaseDate(case.expAgeOfMostRecentDateWithHighRisk)
+            aggregatedRiskResult.mostRecentDateWithLowRisk shouldBe getTestCaseDate(case.expAgeOfMostRecentDateWithLowRisk)
+            aggregatedRiskResult.totalMinimumDistinctEncountersWithHighRisk shouldBe case.expTotalMinimumDistinctEncountersWithHighRisk
+            aggregatedRiskResult.totalMinimumDistinctEncountersWithLowRisk shouldBe case.expTotalMinimumDistinctEncountersWithLowRisk
+        }
+    }
+
+    private fun getTestCaseDate(expAge: Long?): Instant? {
+        if (expAge == null) return null
+        return timeStamper.nowUTC - expAge * DateTimeConstants.MILLIS_PER_DAY
+    }
+
+    private fun comparisonDebugTable(aggregated: AggregatedRiskResult, case: TestCase): String {
+        val result = StringBuilder()
+        result.append("\n").append(case.description)
+        result.append("\n").append("+----------------------+--------------------------+--------------------------+")
+        result.append("\n").append("| Property             | Actual                   | Expected                 |")
+        result.append("\n").append("+----------------------+--------------------------+--------------------------+")
+        result.append(
+            addPropertyCheckToComparisonDebugTable(
+                "Total Risk",
+                aggregated.totalRiskLevel.number,
+                case.expTotalRiskLevel
+            )
+        )
+        result.append(
+            addPropertyCheckToComparisonDebugTable(
+                "Date With High Risk",
+                aggregated.mostRecentDateWithHighRisk,
+                getTestCaseDate(case.expAgeOfMostRecentDateWithHighRisk)
+            )
+        )
+        result.append(
+            addPropertyCheckToComparisonDebugTable(
+                "Date With Low Risk",
+                aggregated.mostRecentDateWithLowRisk,
+                getTestCaseDate(case.expAgeOfMostRecentDateWithLowRisk)
+            )
+        )
+        result.append(
+            addPropertyCheckToComparisonDebugTable(
+                "Encounters High Risk",
+                aggregated.totalMinimumDistinctEncountersWithHighRisk,
+                case.expTotalMinimumDistinctEncountersWithHighRisk
+            )
+        )
+        result.append(
+            addPropertyCheckToComparisonDebugTable(
+                "Encounters Low Risk",
+                aggregated.totalMinimumDistinctEncountersWithLowRisk,
+                case.expTotalMinimumDistinctEncountersWithLowRisk
+            )
+        )
+        result.append("\n")
+        return result.toString()
+    }
+
+    private fun addPropertyCheckToComparisonDebugTable(propertyName: String, expected: Any?, actual: Any?): String {
+        val format = "| %-20s | %-24s | %-24s |"
+        val result = StringBuilder()
+        result.append("\n").append(String.format(format, propertyName, expected, actual))
+        result.append("\n").append("+----------------------+--------------------------+--------------------------+")
+        return result.toString()
+    }
+
+    private fun checkTestCase(case: TestCase) {
+        debugLog("Checking ${case.description}", LogLevel.ALL)
+        case.expTotalRiskLevel shouldNotBe null
+        case.expTotalMinimumDistinctEncountersWithLowRisk shouldNotBe null
+        case.expTotalMinimumDistinctEncountersWithHighRisk shouldNotBe null
+        case.exposureWindows.map { exposureWindow -> checkExposureWindow(exposureWindow) }
+    }
+
+    private fun checkExposureWindow(jsonWindow: JsonWindow) {
+        jsonWindow.ageInDays shouldNotBe null
+        jsonWindow.reportType shouldNotBe null
+        jsonWindow.infectiousness shouldNotBe null
+        jsonWindow.calibrationConfidence shouldNotBe null
+    }
+
+    private fun logConfiguration(config: ConfigData) {
+        val result = StringBuilder()
+        result.append("\n\n").append("----------------- \uD83D\uDEE0 CONFIGURATION \uD83D\uDEE0 -----------")
+
+        result.append("\n").append("â—¦ Minutes At Attenuation Filters (${config.minutesAtAttenuationFilters.size})")
+        for (filter: RiskCalculationParametersOuterClass.MinutesAtAttenuationFilter in config.minutesAtAttenuationFilters) {
+            result.append("\n\t").append("⇥ Filter")
+            result.append(logRange(filter.attenuationRange, "Attenuation Range"))
+            result.append(logRange(filter.dropIfMinutesInRange, "Drop If Minutes In Range"))
+        }
+
+        result.append("\n").append("â—¦ Minutes At Attenuation Weights (${config.minutesAtAttenuationWeights.size})")
+        for (weight: RiskCalculationParametersOuterClass.MinutesAtAttenuationWeight in config.minutesAtAttenuationWeights) {
+            result.append("\n\t").append("⇥ Weight")
+            result.append(logRange(weight.attenuationRange, "Attenuation Range"))
+            result.append("\n\t\t").append("↳ Weight: ${weight.weight}")
+        }
+
+        result.append("\n").append("â—¦ Normalized Time Per Day To Risk Level Mapping List (${config.normalizedTimePerDayToRiskLevelMappingList.size})")
+        for (mapping: RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping in config.normalizedTimePerDayToRiskLevelMappingList) {
+            result.append("\n\t").append("⇥ Mapping")
+            result.append(logRange(mapping.normalizedTimeRange, "Normalized Time Range"))
+            result.append("\n\t\t").append("↳ Risk Level: ${mapping.riskLevel}")
+        }
+
+        result.append("\n").append("â—¦ Normalized Time Per Exposure Window To Risk Level Mapping (${config.normalizedTimePerExposureWindowToRiskLevelMapping.size})")
+        for (mapping: RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping in config.normalizedTimePerExposureWindowToRiskLevelMapping) {
+            result.append("\n\t").append("⇥ Mapping")
+            result.append(logRange(mapping.normalizedTimeRange, "Normalized Time Range"))
+            result.append("\n\t\t").append("↳ Risk Level: ${mapping.riskLevel}")
+        }
+
+        result.append("\n").append("â—¦ Transmission Risk Level Encoding:")
+        result.append("\n\t").append("↳ Infectiousness Offset High: ${config.transmissionRiskLevelEncoding.infectiousnessOffsetHigh}")
+        result.append("\n\t").append("↳ Infectiousness Offset Standard: ${config.transmissionRiskLevelEncoding.infectiousnessOffsetStandard}")
+        result.append("\n\t").append("↳ Report Type Offset Confirmed Clinical Diagnosis: ${config.transmissionRiskLevelEncoding.reportTypeOffsetConfirmedClinicalDiagnosis}")
+        result.append("\n\t").append("↳ Report Type Offset Confirmed Test: ${config.transmissionRiskLevelEncoding.reportTypeOffsetConfirmedTest}")
+        result.append("\n\t").append("↳ Report Type Offset Recursive: ${config.transmissionRiskLevelEncoding.reportTypeOffsetRecursive}")
+        result.append("\n\t").append("↳ Report Type Offset Self Report: ${config.transmissionRiskLevelEncoding.reportTypeOffsetSelfReport}")
+
+        result.append("\n").append("â—¦ Transmission Risk Level Filters (${config.transmissionRiskLevelFilters.size})")
+        for (filter: RiskCalculationParametersOuterClass.TrlFilter in config.transmissionRiskLevelFilters) {
+            result.append("\n\t").append("⇥ Trl Filter")
+            result.append(logRange(filter.dropIfTrlInRange, "Drop If Trl In Range"))
+        }
+
+        result.append("\n").append("â—¦ Transmission Risk Level Multiplier: ${config.transmissionRiskLevelMultiplier}")
+        result.append("\n").append("-------------------------------------------- âš™ -").append("\n")
+        debugLog(result.toString(), LogLevel.NONE)
+    }
+
+    private fun logRange(range: RiskCalculationParametersOuterClass.Range, rangeName: String): String {
+        val builder = StringBuilder()
+        builder.append("\n\t\t").append("⇥ $rangeName")
+        builder.append("\n\t\t\t").append("↳ Min: ${range.min}")
+        builder.append("\n\t\t\t").append("↳ Max: ${range.max}")
+        builder.append("\n\t\t\t").append("↳ Min Exclusive: ${range.minExclusive}")
+        builder.append("\n\t\t\t").append("↳ Max Exclusive: ${range.maxExclusive}")
+        return builder.toString()
+    }
+
+    private fun logExposureWindow(exposureWindow: ExposureWindow, title: String, logLevel: LogLevel = LogLevel.ALL) {
+        val result = StringBuilder()
+        result.append("\n\n").append("------------ $title -----------")
+        result.append("\n").append("Mocked Exposure window: #${exposureWindow.hashCode()}")
+        result.append("\n").append("â—¦ Calibration Confidence: ${exposureWindow.calibrationConfidence}")
+        result.append("\n").append("â—¦ Date Millis Since Epoch: ${exposureWindow.dateMillisSinceEpoch}")
+        result.append("\n").append("â—¦ Infectiousness: ${exposureWindow.infectiousness}")
+        result.append("\n").append("â—¦ Report type: ${exposureWindow.reportType}")
+
+        result.append("\n").append("‣ Scan Instances (${exposureWindow.scanInstances.size}):")
+        for (scan: ScanInstance in exposureWindow.scanInstances) {
+            result.append("\n\t").append("⇥ Mocked Scan Instance: #${scan.hashCode()}")
+            result.append("\n\t\t").append("↳ Min Attenuation: ${scan.minAttenuationDb}")
+            result.append("\n\t\t").append("↳ Seconds Since Last Scan: ${scan.secondsSinceLastScan}")
+            result.append("\n\t\t").append("↳ Typical Attenuation: ${scan.typicalAttenuationDb}")
+        }
+        result.append("\n").append("-------------------------------------------- ✂ ----").append("\n")
+        debugLog(result.toString(), logLevel)
+    }
+
+    private fun jsonToConfiguration(json: DefaultRiskCalculationConfiguration) {
+
+        testConfig = DefaultConfigData(
+            serverTime = Instant.now(),
+            localOffset = Duration.ZERO,
+            mappedConfig = configData,
+            isFallback = false
+        )
+
+        val attenuationFilters = mutableListOf<RiskCalculationParametersOuterClass.MinutesAtAttenuationFilter>()
+        for (jsonFilter: JsonMinutesAtAttenuationFilter in json.minutesAtAttenuationFilters) {
+            val filter: RiskCalculationParametersOuterClass.MinutesAtAttenuationFilter = mockk()
+            every { filter.attenuationRange.min } returns jsonFilter.attenuationRange.min
+            every { filter.attenuationRange.max } returns jsonFilter.attenuationRange.max
+            every { filter.attenuationRange.minExclusive } returns jsonFilter.attenuationRange.minExclusive
+            every { filter.attenuationRange.maxExclusive } returns jsonFilter.attenuationRange.maxExclusive
+            every { filter.dropIfMinutesInRange.min } returns jsonFilter.dropIfMinutesInRange.min
+            every { filter.dropIfMinutesInRange.max } returns jsonFilter.dropIfMinutesInRange.max
+            every { filter.dropIfMinutesInRange.minExclusive } returns jsonFilter.dropIfMinutesInRange.minExclusive
+            every { filter.dropIfMinutesInRange.maxExclusive } returns jsonFilter.dropIfMinutesInRange.maxExclusive
+            attenuationFilters.add(filter)
+        }
+        every { testConfig.minutesAtAttenuationFilters } returns attenuationFilters
+
+        val attenuationWeights = mutableListOf<RiskCalculationParametersOuterClass.MinutesAtAttenuationWeight>()
+        for (jsonWeight: JsonMinutesAtAttenuationWeight in json.minutesAtAttenuationWeights) {
+            val weight: RiskCalculationParametersOuterClass.MinutesAtAttenuationWeight = mockk()
+            every { weight.attenuationRange.min } returns jsonWeight.attenuationRange.min
+            every { weight.attenuationRange.max } returns jsonWeight.attenuationRange.max
+            every { weight.attenuationRange.minExclusive } returns jsonWeight.attenuationRange.minExclusive
+            every { weight.attenuationRange.maxExclusive } returns jsonWeight.attenuationRange.maxExclusive
+            every { weight.weight } returns jsonWeight.weight
+            attenuationWeights.add(weight)
+        }
+        every { testConfig.minutesAtAttenuationWeights } returns attenuationWeights
+
+        val normalizedTimePerDayToRiskLevelMapping = mutableListOf< RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping>()
+        for (jsonMapping: JsonNormalizedTimeToRiskLevelMapping in json.normalizedTimePerDayToRiskLevelMapping) {
+            val mapping: RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping = mockk()
+            every { mapping.riskLevel } returns RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping.RiskLevel.forNumber(jsonMapping.riskLevel)
+            every { mapping.normalizedTimeRange.min } returns jsonMapping.normalizedTimeRange.min
+            every { mapping.normalizedTimeRange.max } returns jsonMapping.normalizedTimeRange.max
+            every { mapping.normalizedTimeRange.minExclusive } returns jsonMapping.normalizedTimeRange.minExclusive
+            every { mapping.normalizedTimeRange.maxExclusive } returns jsonMapping.normalizedTimeRange.maxExclusive
+            normalizedTimePerDayToRiskLevelMapping.add(mapping)
+        }
+        every { testConfig.normalizedTimePerDayToRiskLevelMappingList } returns normalizedTimePerDayToRiskLevelMapping
+
+        val normalizedTimePerExposureWindowToRiskLevelMapping = mutableListOf< RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping>()
+        for (jsonMapping: JsonNormalizedTimeToRiskLevelMapping in json.normalizedTimePerEWToRiskLevelMapping) {
+            val mapping: RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping = mockk()
+            every { mapping.riskLevel } returns RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping.RiskLevel.forNumber(jsonMapping.riskLevel)
+            every { mapping.normalizedTimeRange.min } returns jsonMapping.normalizedTimeRange.min
+            every { mapping.normalizedTimeRange.max } returns jsonMapping.normalizedTimeRange.max
+            every { mapping.normalizedTimeRange.minExclusive } returns jsonMapping.normalizedTimeRange.minExclusive
+            every { mapping.normalizedTimeRange.maxExclusive } returns jsonMapping.normalizedTimeRange.maxExclusive
+            normalizedTimePerExposureWindowToRiskLevelMapping.add(mapping)
+        }
+        every { testConfig.normalizedTimePerExposureWindowToRiskLevelMapping } returns normalizedTimePerExposureWindowToRiskLevelMapping
+
+        every { testConfig.transmissionRiskLevelMultiplier } returns json.transmissionRiskLevelMultiplier
+
+        val trlEncoding: RiskCalculationParametersOuterClass.TransmissionRiskLevelEncoding = mockk()
+        every { trlEncoding.infectiousnessOffsetHigh } returns json.trlEncoding.infectiousnessOffsetHigh
+        every { trlEncoding.infectiousnessOffsetStandard } returns json.trlEncoding.infectiousnessOffsetStandard
+        every { trlEncoding.reportTypeOffsetConfirmedClinicalDiagnosis } returns json.trlEncoding.reportTypeOffsetConfirmedClinicalDiagnosis
+        every { trlEncoding.reportTypeOffsetConfirmedTest } returns json.trlEncoding.reportTypeOffsetConfirmedTest
+        every { trlEncoding.reportTypeOffsetRecursive } returns json.trlEncoding.reportTypeOffsetRecursive
+        every { trlEncoding.reportTypeOffsetSelfReport } returns json.trlEncoding.reportTypeOffsetSelfReport
+        every { testConfig.transmissionRiskLevelEncoding } returns trlEncoding
+
+        val trlFilters = mutableListOf<RiskCalculationParametersOuterClass.TrlFilter>()
+        for (jsonFilter: JsonTrlFilter in json.trlFilters) {
+            val filter: RiskCalculationParametersOuterClass.TrlFilter = mockk()
+            every { filter.dropIfTrlInRange.min } returns jsonFilter.dropIfTrlInRange.min
+            every { filter.dropIfTrlInRange.max } returns jsonFilter.dropIfTrlInRange.max
+            every { filter.dropIfTrlInRange.minExclusive } returns jsonFilter.dropIfTrlInRange.minExclusive
+            every { filter.dropIfTrlInRange.maxExclusive } returns jsonFilter.dropIfTrlInRange.maxExclusive
+            trlFilters.add(filter)
+        }
+        every { testConfig.transmissionRiskLevelFilters } returns trlFilters
+    }
+
+    private fun jsonToExposureWindow(json: JsonWindow): ExposureWindow {
+        val exposureWindow: ExposureWindow = mockk()
+
+        every { exposureWindow.calibrationConfidence } returns json.calibrationConfidence
+        every { exposureWindow.dateMillisSinceEpoch } returns timeStamper.nowUTC.millis - (DateTimeConstants.MILLIS_PER_DAY * json.ageInDays).toLong()
+        every { exposureWindow.infectiousness } returns json.infectiousness
+        every { exposureWindow.reportType } returns json.reportType
+        every { exposureWindow.scanInstances } returns json.scanInstances.map { scanInstance ->
+            jsonToScanInstance(
+                scanInstance
+            )
+        }
+
+        logExposureWindow(exposureWindow, "⊞ EXPOSURE WINDOW MOCK ⊞")
+
+        return exposureWindow
+    }
+
+    private fun jsonToScanInstance(json: JsonScanInstance): ScanInstance {
+        val scanInstance: ScanInstance = mockk()
+        every { scanInstance.minAttenuationDb } returns json.minAttenuation
+        every { scanInstance.secondsSinceLastScan } returns json.secondsSinceLastScan
+        every { scanInstance.typicalAttenuationDb } returns json.typicalAttenuation
+        return scanInstance
+    }
+}
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/ExposureWindowsJsonInput.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/ExposureWindowsJsonInput.kt
new file mode 100644
index 000000000..3c0a77a3a
--- /dev/null
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/ExposureWindowsJsonInput.kt
@@ -0,0 +1,14 @@
+package de.rki.coronawarnapp.nearby.windows.entities
+
+import com.google.gson.annotations.SerializedName
+import de.rki.coronawarnapp.nearby.windows.entities.cases.TestCase
+import de.rki.coronawarnapp.nearby.windows.entities.configuration.DefaultRiskCalculationConfiguration
+
+data class ExposureWindowsJsonInput(
+    @SerializedName("__comment__")
+    val comment: String,
+    @SerializedName("defaultRiskCalculationConfiguration")
+    val defaultRiskCalculationConfiguration: DefaultRiskCalculationConfiguration,
+    @SerializedName("testCases")
+    val testCases: List<TestCase>
+)
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/cases/JsonScanInstance.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/cases/JsonScanInstance.kt
new file mode 100644
index 000000000..a9ea714b7
--- /dev/null
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/cases/JsonScanInstance.kt
@@ -0,0 +1,12 @@
+package de.rki.coronawarnapp.nearby.windows.entities.cases
+
+import com.google.gson.annotations.SerializedName
+
+data class JsonScanInstance(
+    @SerializedName("minAttenuation")
+    val minAttenuation: Int,
+    @SerializedName("secondsSinceLastScan")
+    val secondsSinceLastScan: Int,
+    @SerializedName("typicalAttenuation")
+    val typicalAttenuation: Int
+)
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/cases/JsonWindow.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/cases/JsonWindow.kt
new file mode 100644
index 000000000..7394b3295
--- /dev/null
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/cases/JsonWindow.kt
@@ -0,0 +1,16 @@
+package de.rki.coronawarnapp.nearby.windows.entities.cases
+
+import com.google.gson.annotations.SerializedName
+
+data class JsonWindow(
+    @SerializedName("ageInDays")
+    val ageInDays: Int,
+    @SerializedName("calibrationConfidence")
+    val calibrationConfidence: Int,
+    @SerializedName("infectiousness")
+    val infectiousness: Int,
+    @SerializedName("reportType")
+    val reportType: Int,
+    @SerializedName("scanInstances")
+    val scanInstances: List<JsonScanInstance>
+)
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/cases/TestCase.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/cases/TestCase.kt
new file mode 100644
index 000000000..fa8f45277
--- /dev/null
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/cases/TestCase.kt
@@ -0,0 +1,20 @@
+package de.rki.coronawarnapp.nearby.windows.entities.cases
+
+import com.google.gson.annotations.SerializedName
+
+data class TestCase(
+    @SerializedName("description")
+    val description: String,
+    @SerializedName("expAgeOfMostRecentDateWithHighRisk")
+    val expAgeOfMostRecentDateWithHighRisk: Long?,
+    @SerializedName("expAgeOfMostRecentDateWithLowRisk")
+    val expAgeOfMostRecentDateWithLowRisk: Long?,
+    @SerializedName("expTotalMinimumDistinctEncountersWithHighRisk")
+    val expTotalMinimumDistinctEncountersWithHighRisk: Int,
+    @SerializedName("expTotalMinimumDistinctEncountersWithLowRisk")
+    val expTotalMinimumDistinctEncountersWithLowRisk: Int,
+    @SerializedName("expTotalRiskLevel")
+    val expTotalRiskLevel: Int,
+    @SerializedName("exposureWindows")
+    val exposureWindows: List<JsonWindow>
+)
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/configuration/DefaultRiskCalculationConfiguration.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/configuration/DefaultRiskCalculationConfiguration.kt
new file mode 100644
index 000000000..7445d86e7
--- /dev/null
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/configuration/DefaultRiskCalculationConfiguration.kt
@@ -0,0 +1,20 @@
+package de.rki.coronawarnapp.nearby.windows.entities.configuration
+
+import com.google.gson.annotations.SerializedName
+
+data class DefaultRiskCalculationConfiguration(
+    @SerializedName("minutesAtAttenuationFilters")
+    val minutesAtAttenuationFilters: List<JsonMinutesAtAttenuationFilter>,
+    @SerializedName("minutesAtAttenuationWeights")
+    val minutesAtAttenuationWeights: List<JsonMinutesAtAttenuationWeight>,
+    @SerializedName("normalizedTimePerDayToRiskLevelMapping")
+    val normalizedTimePerDayToRiskLevelMapping: List<JsonNormalizedTimeToRiskLevelMapping>,
+    @SerializedName("normalizedTimePerEWToRiskLevelMapping")
+    val normalizedTimePerEWToRiskLevelMapping: List<JsonNormalizedTimeToRiskLevelMapping>,
+    @SerializedName("transmissionRiskLevelMultiplier")
+    val transmissionRiskLevelMultiplier: Double,
+    @SerializedName("trlEncoding")
+    val trlEncoding: JsonTrlEncoding,
+    @SerializedName("trlFilters")
+    val trlFilters: List<JsonTrlFilter>
+)
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/configuration/JsonMinutesAtAttenuationFilter.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/configuration/JsonMinutesAtAttenuationFilter.kt
new file mode 100644
index 000000000..a01efc1f6
--- /dev/null
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/configuration/JsonMinutesAtAttenuationFilter.kt
@@ -0,0 +1,10 @@
+package de.rki.coronawarnapp.nearby.windows.entities.configuration
+
+import com.google.gson.annotations.SerializedName
+
+data class JsonMinutesAtAttenuationFilter(
+    @SerializedName("attenuationRange")
+    val attenuationRange: Range,
+    @SerializedName("dropIfMinutesInRange")
+    val dropIfMinutesInRange: Range
+)
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/configuration/JsonMinutesAtAttenuationWeight.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/configuration/JsonMinutesAtAttenuationWeight.kt
new file mode 100644
index 000000000..3af598da7
--- /dev/null
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/configuration/JsonMinutesAtAttenuationWeight.kt
@@ -0,0 +1,10 @@
+package de.rki.coronawarnapp.nearby.windows.entities.configuration
+
+import com.google.gson.annotations.SerializedName
+
+data class JsonMinutesAtAttenuationWeight(
+    @SerializedName("attenuationRange")
+    val attenuationRange: Range,
+    @SerializedName("weight")
+    val weight: Double
+)
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/configuration/JsonNormalizedTimeToRiskLevelMapping.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/configuration/JsonNormalizedTimeToRiskLevelMapping.kt
new file mode 100644
index 000000000..4f0fc7869
--- /dev/null
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/configuration/JsonNormalizedTimeToRiskLevelMapping.kt
@@ -0,0 +1,10 @@
+package de.rki.coronawarnapp.nearby.windows.entities.configuration
+
+import com.google.gson.annotations.SerializedName
+
+data class JsonNormalizedTimeToRiskLevelMapping(
+    @SerializedName("normalizedTimeRange")
+    val normalizedTimeRange: Range,
+    @SerializedName("riskLevel")
+    val riskLevel: Int
+)
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/configuration/JsonTrlEncoding.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/configuration/JsonTrlEncoding.kt
new file mode 100644
index 000000000..00a3e5af7
--- /dev/null
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/configuration/JsonTrlEncoding.kt
@@ -0,0 +1,18 @@
+package de.rki.coronawarnapp.nearby.windows.entities.configuration
+
+import com.google.gson.annotations.SerializedName
+
+data class JsonTrlEncoding(
+    @SerializedName("infectiousnessOffsetHigh")
+    val infectiousnessOffsetHigh: Int,
+    @SerializedName("infectiousnessOffsetStandard")
+    val infectiousnessOffsetStandard: Int,
+    @SerializedName("reportTypeOffsetConfirmedClinicalDiagnosis")
+    val reportTypeOffsetConfirmedClinicalDiagnosis: Int,
+    @SerializedName("reportTypeOffsetConfirmedTest")
+    val reportTypeOffsetConfirmedTest: Int,
+    @SerializedName("reportTypeOffsetRecursive")
+    val reportTypeOffsetRecursive: Int,
+    @SerializedName("reportTypeOffsetSelfReport")
+    val reportTypeOffsetSelfReport: Int
+)
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/configuration/JsonTrlFilter.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/configuration/JsonTrlFilter.kt
new file mode 100644
index 000000000..6e529bc6a
--- /dev/null
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/configuration/JsonTrlFilter.kt
@@ -0,0 +1,8 @@
+package de.rki.coronawarnapp.nearby.windows.entities.configuration
+
+import com.google.gson.annotations.SerializedName
+
+data class JsonTrlFilter(
+    @SerializedName("dropIfTrlInRange")
+    val dropIfTrlInRange: Range
+)
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/configuration/Range.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/configuration/Range.kt
new file mode 100644
index 000000000..a4d686a74
--- /dev/null
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/configuration/Range.kt
@@ -0,0 +1,14 @@
+package de.rki.coronawarnapp.nearby.windows.entities.configuration
+
+import com.google.gson.annotations.SerializedName
+
+data class Range(
+    @SerializedName("min")
+    val min: Double,
+    @SerializedName("minExclusive")
+    val minExclusive: Boolean,
+    @SerializedName("max")
+    val max: Double,
+    @SerializedName("maxExclusive")
+    val maxExclusive: Boolean
+)
diff --git a/Corona-Warn-App/src/test/resources/exposure-windows-risk-calculation.json b/Corona-Warn-App/src/test/resources/exposure-windows-risk-calculation.json
new file mode 100644
index 000000000..4338b8f94
--- /dev/null
+++ b/Corona-Warn-App/src/test/resources/exposure-windows-risk-calculation.json
@@ -0,0 +1,1025 @@
+{
+  "__comment__": "JSON has been generated from YAML, see README",
+  "defaultRiskCalculationConfiguration": {
+    "minutesAtAttenuationFilters": [
+      {
+        "attenuationRange": {
+          "min": 0,
+          "max": 73,
+          "maxExclusive": true
+        },
+        "dropIfMinutesInRange": {
+          "min": 0,
+          "max": 10,
+          "maxExclusive": true
+        }
+      }
+    ],
+    "trlFilters": [
+      {
+        "dropIfTrlInRange": {
+          "min": 1,
+          "max": 2
+        }
+      }
+    ],
+    "minutesAtAttenuationWeights": [
+      {
+        "attenuationRange": {
+          "min": 0,
+          "max": 55,
+          "maxExclusive": true
+        },
+        "weight": 1
+      },
+      {
+        "attenuationRange": {
+          "min": 55,
+          "max": 63,
+          "maxExclusive": true
+        },
+        "weight": 0.5
+      }
+    ],
+    "normalizedTimePerEWToRiskLevelMapping": [
+      {
+        "normalizedTimeRange": {
+          "min": 0,
+          "max": 15,
+          "maxExclusive": true
+        },
+        "riskLevel": 1
+      },
+      {
+        "normalizedTimeRange": {
+          "min": 15,
+          "max": 9999
+        },
+        "riskLevel": 2
+      }
+    ],
+    "normalizedTimePerDayToRiskLevelMapping": [
+      {
+        "normalizedTimeRange": {
+          "min": 0,
+          "max": 15,
+          "maxExclusive": true
+        },
+        "riskLevel": 1
+      },
+      {
+        "normalizedTimeRange": {
+          "min": 15,
+          "max": 9999
+        },
+        "riskLevel": 2
+      }
+    ],
+    "trlEncoding": {
+      "infectiousnessOffsetStandard": 0,
+      "infectiousnessOffsetHigh":  4,
+      "reportTypeOffsetRecursive": 4,
+      "reportTypeOffsetSelfReport": 3,
+      "reportTypeOffsetConfirmedClinicalDiagnosis": 2,
+      "reportTypeOffsetConfirmedTest": 1
+    },
+    "transmissionRiskLevelMultiplier": 0.2
+  },
+  "testCases": [
+    {
+      "description": "drop Exposure Windows that do not match minutesAtAttenuationFilters (< 10 minutes)",
+      "exposureWindows": [
+        {
+          "ageInDays": 1,
+          "reportType": 2,
+          "infectiousness": 2,
+          "calibrationConfidence": 0,
+          "scanInstances": [
+            {
+              "typicalAttenuation": 30,
+              "minAttenuation": 25,
+              "secondsSinceLastScan": 300
+            },
+            {
+              "typicalAttenuation": 30,
+              "minAttenuation": 25,
+              "secondsSinceLastScan": 299
+            }
+          ]
+        }
+      ],
+      "expTotalRiskLevel": 1,
+      "expTotalMinimumDistinctEncountersWithLowRisk": 0,
+      "expAgeOfMostRecentDateWithLowRisk": null,
+      "expAgeOfMostRecentDateWithHighRisk": null,
+      "expTotalMinimumDistinctEncountersWithHighRisk": 0
+    },
+    {
+      "description": "keep Exposure Windows that match minutesAtAttenuationFilters (>= 10 minutes)",
+      "exposureWindows": [
+        {
+          "ageInDays": 1,
+          "reportType": 2,
+          "infectiousness": 2,
+          "calibrationConfidence": 0,
+          "scanInstances": [
+            {
+              "typicalAttenuation": 30,
+              "minAttenuation": 25,
+              "secondsSinceLastScan": 300
+            },
+            {
+              "typicalAttenuation": 30,
+              "minAttenuation": 25,
+              "secondsSinceLastScan": 300
+            }
+          ]
+        }
+      ],
+      "expTotalRiskLevel": 1,
+      "expTotalMinimumDistinctEncountersWithLowRisk": 1,
+      "expAgeOfMostRecentDateWithLowRisk": 1,
+      "expAgeOfMostRecentDateWithHighRisk": null,
+      "expTotalMinimumDistinctEncountersWithHighRisk": 0
+    },
+    {
+      "description": "drop Exposure Windows that do not match minutesAtAttenuationFilters (>= 73 dB)",
+      "exposureWindows": [
+        {
+          "ageInDays": 1,
+          "reportType": 2,
+          "infectiousness": 2,
+          "calibrationConfidence": 0,
+          "scanInstances": [
+            {
+              "typicalAttenuation": 73,
+              "minAttenuation": 25,
+              "secondsSinceLastScan": 300
+            },
+            {
+              "typicalAttenuation": 73,
+              "minAttenuation": 25,
+              "secondsSinceLastScan": 300
+            }
+          ]
+        }
+      ],
+      "expTotalRiskLevel": 1,
+      "expTotalMinimumDistinctEncountersWithLowRisk": 0,
+      "expAgeOfMostRecentDateWithLowRisk": null,
+      "expAgeOfMostRecentDateWithHighRisk": null,
+      "expTotalMinimumDistinctEncountersWithHighRisk": 0
+    },
+    {
+      "description": "keep Exposure Windows that match minutesAtAttenuationFilters (< 73 dB)",
+      "exposureWindows": [
+        {
+          "ageInDays": 1,
+          "reportType": 2,
+          "infectiousness": 2,
+          "calibrationConfidence": 0,
+          "scanInstances": [
+            {
+              "typicalAttenuation": 72,
+              "minAttenuation": 25,
+              "secondsSinceLastScan": 300
+            },
+            {
+              "typicalAttenuation": 72,
+              "minAttenuation": 25,
+              "secondsSinceLastScan": 300
+            }
+          ]
+        }
+      ],
+      "expTotalRiskLevel": 1,
+      "expTotalMinimumDistinctEncountersWithLowRisk": 1,
+      "expAgeOfMostRecentDateWithLowRisk": 1,
+      "expAgeOfMostRecentDateWithHighRisk": null,
+      "expTotalMinimumDistinctEncountersWithHighRisk": 0
+    },
+    {
+      "description": "drop Exposure Windows that do not match trlFilters (<= 2)",
+      "exposureWindows": [
+        {
+          "ageInDays": 1,
+          "reportType": 2,
+          "infectiousness": 1,
+          "calibrationConfidence": 0,
+          "scanInstances": [
+            {
+              "typicalAttenuation": 30,
+              "minAttenuation": 25,
+              "secondsSinceLastScan": 300
+            },
+            {
+              "typicalAttenuation": 30,
+              "minAttenuation": 25,
+              "secondsSinceLastScan": 300
+            }
+          ]
+        }
+      ],
+      "expTotalRiskLevel": 1,
+      "expTotalMinimumDistinctEncountersWithLowRisk": 0,
+      "expAgeOfMostRecentDateWithLowRisk": null,
+      "expAgeOfMostRecentDateWithHighRisk": null,
+      "expTotalMinimumDistinctEncountersWithHighRisk": 0
+    },
+    {
+      "description": "keep Exposure Windows that match trlFilters (> 2)",
+      "exposureWindows": [
+        {
+          "ageInDays": 1,
+          "reportType": 3,
+          "infectiousness": 1,
+          "calibrationConfidence": 0,
+          "scanInstances": [
+            {
+              "typicalAttenuation": 30,
+              "minAttenuation": 25,
+              "secondsSinceLastScan": 300
+            },
+            {
+              "typicalAttenuation": 30,
+              "minAttenuation": 25,
+              "secondsSinceLastScan": 300
+            }
+          ]
+        }
+      ],
+      "expTotalRiskLevel": 1,
+      "expTotalMinimumDistinctEncountersWithLowRisk": 1,
+      "expAgeOfMostRecentDateWithLowRisk": 1,
+      "expAgeOfMostRecentDateWithHighRisk": null,
+      "expTotalMinimumDistinctEncountersWithHighRisk": 0
+    },
+    {
+      "description": "identify Exposure Window as Low Risk based on normalizedTime (< 15)",
+      "exposureWindows": [
+        {
+          "ageInDays": 1,
+          "reportType": 1,
+          "infectiousness": 2,
+          "calibrationConfidence": 0,
+          "scanInstances": [
+            {
+              "typicalAttenuation": 30,
+              "minAttenuation": 25,
+              "secondsSinceLastScan": 300
+            },
+            {
+              "typicalAttenuation": 30,
+              "minAttenuation": 25,
+              "secondsSinceLastScan": 300
+            },
+            {
+              "typicalAttenuation": 30,
+              "minAttenuation": 25,
+              "secondsSinceLastScan": 299
+            }
+          ]
+        }
+      ],
+      "expTotalRiskLevel": 1,
+      "expTotalMinimumDistinctEncountersWithLowRisk": 1,
+      "expAgeOfMostRecentDateWithLowRisk": 1,
+      "expAgeOfMostRecentDateWithHighRisk": null,
+      "expTotalMinimumDistinctEncountersWithHighRisk": 0,
+      "expNumberOfExposureWindowsWithLowRisk": 1,
+      "expNumberOfExposureWindowsWithHighRisk": 0
+    },
+    {
+      "description": "identify Exposure Window as High Risk based on normalizedTime (>= 15)",
+      "exposureWindows": [
+        {
+          "ageInDays": 1,
+          "reportType": 1,
+          "infectiousness": 2,
+          "calibrationConfidence": 0,
+          "scanInstances": [
+            {
+              "typicalAttenuation": 30,
+              "minAttenuation": 25,
+              "secondsSinceLastScan": 300
+            },
+            {
+              "typicalAttenuation": 30,
+              "minAttenuation": 25,
+              "secondsSinceLastScan": 300
+            },
+            {
+              "typicalAttenuation": 30,
+              "minAttenuation": 25,
+              "secondsSinceLastScan": 300
+            }
+          ]
+        }
+      ],
+      "expTotalRiskLevel": 2,
+      "expTotalMinimumDistinctEncountersWithLowRisk": 0,
+      "expAgeOfMostRecentDateWithLowRisk": null,
+      "expAgeOfMostRecentDateWithHighRisk": 1,
+      "expTotalMinimumDistinctEncountersWithHighRisk": 1,
+      "expNumberOfExposureWindowsWithLowRisk": 1,
+      "expNumberOfExposureWindowsWithHighRisk": 0
+    },
+    {
+      "description": "identify the most recent date with Low Risk",
+      "exposureWindows": [
+        {
+          "ageInDays": 3,
+          "reportType": 3,
+          "infectiousness": 1,
+          "calibrationConfidence": 0,
+          "scanInstances": [
+            {
+              "typicalAttenuation": 30,
+              "minAttenuation": 25,
+              "secondsSinceLastScan": 300
+            },
+            {
+              "typicalAttenuation": 30,
+              "minAttenuation": 25,
+              "secondsSinceLastScan": 300
+            }
+          ]
+        },
+        {
+          "ageInDays": 2,
+          "reportType": 3,
+          "infectiousness": 1,
+          "calibrationConfidence": 0,
+          "scanInstances": [
+            {
+              "typicalAttenuation": 30,
+              "minAttenuation": 25,
+              "secondsSinceLastScan": 300
+            },
+            {
+              "typicalAttenuation": 30,
+              "minAttenuation": 25,
+              "secondsSinceLastScan": 300
+            }
+          ]
+        },
+        {
+          "ageInDays": 4,
+          "reportType": 3,
+          "infectiousness": 1,
+          "calibrationConfidence": 0,
+          "scanInstances": [
+            {
+              "typicalAttenuation": 30,
+              "minAttenuation": 25,
+              "secondsSinceLastScan": 300
+            },
+            {
+              "typicalAttenuation": 30,
+              "minAttenuation": 25,
+              "secondsSinceLastScan": 300
+            }
+          ]
+        }
+      ],
+      "expTotalRiskLevel": 1,
+      "expTotalMinimumDistinctEncountersWithLowRisk": 3,
+      "expAgeOfMostRecentDateWithLowRisk": 2,
+      "expAgeOfMostRecentDateWithHighRisk": null,
+      "expTotalMinimumDistinctEncountersWithHighRisk": 0
+    },
+    {
+      "description": "count Exposure Windows with same Date/TRL/CallibrationConfidence only once towards distinct encounters with Low Risk",
+      "exposureWindows": [
+        {
+          "ageInDays": 1,
+          "reportType": 3,
+          "infectiousness": 1,
+          "calibrationConfidence": 0,
+          "scanInstances": [
+            {
+              "typicalAttenuation": 30,
+              "minAttenuation": 25,
+              "secondsSinceLastScan": 300
+            },
+            {
+              "typicalAttenuation": 30,
+              "minAttenuation": 25,
+              "secondsSinceLastScan": 300
+            }
+          ]
+        },
+        {
+          "ageInDays": 1,
+          "reportType": 3,
+          "infectiousness": 1,
+          "calibrationConfidence": 0,
+          "scanInstances": [
+            {
+              "typicalAttenuation": 30,
+              "minAttenuation": 25,
+              "secondsSinceLastScan": 300
+            },
+            {
+              "typicalAttenuation": 30,
+              "minAttenuation": 25,
+              "secondsSinceLastScan": 300
+            }
+          ]
+        }
+      ],
+      "expTotalRiskLevel": 1,
+      "expTotalMinimumDistinctEncountersWithLowRisk": 1,
+      "expAgeOfMostRecentDateWithLowRisk": 1,
+      "expAgeOfMostRecentDateWithHighRisk": null,
+      "expTotalMinimumDistinctEncountersWithHighRisk": 0
+    },
+    {
+      "description": "count Exposure Windows with same Date/TRL but different CallibrationConfidence separately towards distinct encounters with Low Risk",
+      "exposureWindows": [
+        {
+          "ageInDays": 1,
+          "reportType": 3,
+          "infectiousness": 1,
+          "calibrationConfidence": 0,
+          "scanInstances": [
+            {
+              "typicalAttenuation": 30,
+              "minAttenuation": 25,
+              "secondsSinceLastScan": 300
+            },
+            {
+              "typicalAttenuation": 30,
+              "minAttenuation": 25,
+              "secondsSinceLastScan": 300
+            }
+          ]
+        },
+        {
+          "ageInDays": 1,
+          "reportType": 3,
+          "infectiousness": 1,
+          "calibrationConfidence": 1,
+          "scanInstances": [
+            {
+              "typicalAttenuation": 30,
+              "minAttenuation": 25,
+              "secondsSinceLastScan": 300
+            },
+            {
+              "typicalAttenuation": 30,
+              "minAttenuation": 25,
+              "secondsSinceLastScan": 300
+            }
+          ]
+        }
+      ],
+      "expTotalRiskLevel": 1,
+      "expTotalMinimumDistinctEncountersWithLowRisk": 2,
+      "expAgeOfMostRecentDateWithLowRisk": 1,
+      "expAgeOfMostRecentDateWithHighRisk": null,
+      "expTotalMinimumDistinctEncountersWithHighRisk": 0
+    },
+    {
+      "description": "count Exposure Windows with same Date/CallibrationConfidence but different TRL separately towards distinct encounters with Low Risk",
+      "exposureWindows": [
+        {
+          "ageInDays": 1,
+          "reportType": 3,
+          "infectiousness": 1,
+          "calibrationConfidence": 0,
+          "scanInstances": [
+            {
+              "typicalAttenuation": 30,
+              "minAttenuation": 25,
+              "secondsSinceLastScan": 300
+            },
+            {
+              "typicalAttenuation": 30,
+              "minAttenuation": 25,
+              "secondsSinceLastScan": 300
+            }
+          ]
+        },
+        {
+          "ageInDays": 1,
+          "reportType": 4,
+          "infectiousness": 1,
+          "calibrationConfidence": 0,
+          "scanInstances": [
+            {
+              "typicalAttenuation": 30,
+              "minAttenuation": 25,
+              "secondsSinceLastScan": 300
+            },
+            {
+              "typicalAttenuation": 30,
+              "minAttenuation": 25,
+              "secondsSinceLastScan": 300
+            }
+          ]
+        }
+      ],
+      "expTotalRiskLevel": 1,
+      "expTotalMinimumDistinctEncountersWithLowRisk": 2,
+      "expAgeOfMostRecentDateWithLowRisk": 1,
+      "expAgeOfMostRecentDateWithHighRisk": null,
+      "expTotalMinimumDistinctEncountersWithHighRisk": 0
+    },
+    {
+      "description": "count Exposure Windows with same TRL/CallibrationConfidence but different Date separately towards distinct encounters with Low Risk",
+      "exposureWindows": [
+        {
+          "ageInDays": 1,
+          "reportType": 3,
+          "infectiousness": 1,
+          "calibrationConfidence": 0,
+          "scanInstances": [
+            {
+              "typicalAttenuation": 30,
+              "minAttenuation": 25,
+              "secondsSinceLastScan": 300
+            },
+            {
+              "typicalAttenuation": 30,
+              "minAttenuation": 25,
+              "secondsSinceLastScan": 300
+            }
+          ]
+        },
+        {
+          "ageInDays": 2,
+          "reportType": 3,
+          "infectiousness": 1,
+          "calibrationConfidence": 0,
+          "scanInstances": [
+            {
+              "typicalAttenuation": 30,
+              "minAttenuation": 25,
+              "secondsSinceLastScan": 300
+            },
+            {
+              "typicalAttenuation": 30,
+              "minAttenuation": 25,
+              "secondsSinceLastScan": 300
+            }
+          ]
+        }
+      ],
+      "expTotalRiskLevel": 1,
+      "expTotalMinimumDistinctEncountersWithLowRisk": 2,
+      "expAgeOfMostRecentDateWithLowRisk": 1,
+      "expAgeOfMostRecentDateWithHighRisk": null,
+      "expTotalMinimumDistinctEncountersWithHighRisk": 0
+    },
+    {
+      "description": "determine High Risk in total if there are sufficient Exposure Windows with a Low Risk",
+      "exposureWindows": [
+        {
+          "ageInDays": 1,
+          "reportType": 3,
+          "infectiousness": 1,
+          "calibrationConfidence": 0,
+          "scanInstances": [
+            {
+              "typicalAttenuation": 30,
+              "minAttenuation": 25,
+              "secondsSinceLastScan": 300
+            },
+            {
+              "typicalAttenuation": 30,
+              "minAttenuation": 25,
+              "secondsSinceLastScan": 300
+            }
+          ]
+        },
+        {
+          "ageInDays": 1,
+          "reportType": 3,
+          "infectiousness": 1,
+          "calibrationConfidence": 0,
+          "scanInstances": [
+            {
+              "typicalAttenuation": 30,
+              "minAttenuation": 25,
+              "secondsSinceLastScan": 300
+            },
+            {
+              "typicalAttenuation": 30,
+              "minAttenuation": 25,
+              "secondsSinceLastScan": 300
+            }
+          ]
+        },
+        {
+          "ageInDays": 1,
+          "reportType": 3,
+          "infectiousness": 1,
+          "calibrationConfidence": 0,
+          "scanInstances": [
+            {
+              "typicalAttenuation": 30,
+              "minAttenuation": 25,
+              "secondsSinceLastScan": 300
+            },
+            {
+              "typicalAttenuation": 30,
+              "minAttenuation": 25,
+              "secondsSinceLastScan": 300
+            }
+          ]
+        }
+      ],
+      "expTotalRiskLevel": 2,
+      "expTotalMinimumDistinctEncountersWithLowRisk": 1,
+      "expAgeOfMostRecentDateWithLowRisk": null,
+      "expAgeOfMostRecentDateWithHighRisk": 1,
+      "expTotalMinimumDistinctEncountersWithHighRisk": 0
+    },
+    {
+      "description": "identify the most recent date with High Risk",
+      "exposureWindows": [
+        {
+          "ageInDays": 3,
+          "reportType": 4,
+          "infectiousness": 2,
+          "calibrationConfidence": 0,
+          "scanInstances": [
+            {
+              "typicalAttenuation": 30,
+              "minAttenuation": 25,
+              "secondsSinceLastScan": 420
+            },
+            {
+              "typicalAttenuation": 30,
+              "minAttenuation": 25,
+              "secondsSinceLastScan": 420
+            }
+          ]
+        },
+        {
+          "ageInDays": 2,
+          "reportType": 4,
+          "infectiousness": 2,
+          "calibrationConfidence": 0,
+          "scanInstances": [
+            {
+              "typicalAttenuation": 30,
+              "minAttenuation": 25,
+              "secondsSinceLastScan": 420
+            },
+            {
+              "typicalAttenuation": 30,
+              "minAttenuation": 25,
+              "secondsSinceLastScan": 420
+            }
+          ]
+        },
+        {
+          "ageInDays": 4,
+          "reportType": 4,
+          "infectiousness": 2,
+          "calibrationConfidence": 0,
+          "scanInstances": [
+            {
+              "typicalAttenuation": 30,
+              "minAttenuation": 25,
+              "secondsSinceLastScan": 420
+            },
+            {
+              "typicalAttenuation": 30,
+              "minAttenuation": 25,
+              "secondsSinceLastScan": 420
+            }
+          ]
+        }
+      ],
+      "expTotalRiskLevel": 2,
+      "expTotalMinimumDistinctEncountersWithLowRisk": 0,
+      "expAgeOfMostRecentDateWithLowRisk": null,
+      "expAgeOfMostRecentDateWithHighRisk": 2,
+      "expTotalMinimumDistinctEncountersWithHighRisk": 3
+    },
+    {
+      "description": "count Exposure Windows with same Date/TRL/CallibrationConfidence only once towards distinct encounters with High Risk",
+      "exposureWindows": [
+        {
+          "ageInDays": 1,
+          "reportType": 4,
+          "infectiousness": 2,
+          "calibrationConfidence": 0,
+          "scanInstances": [
+            {
+              "typicalAttenuation": 30,
+              "minAttenuation": 25,
+              "secondsSinceLastScan": 420
+            },
+            {
+              "typicalAttenuation": 30,
+              "minAttenuation": 25,
+              "secondsSinceLastScan": 420
+            }
+          ]
+        },
+        {
+          "ageInDays": 1,
+          "reportType": 4,
+          "infectiousness": 2,
+          "calibrationConfidence": 0,
+          "scanInstances": [
+            {
+              "typicalAttenuation": 30,
+              "minAttenuation": 25,
+              "secondsSinceLastScan": 420
+            },
+            {
+              "typicalAttenuation": 30,
+              "minAttenuation": 25,
+              "secondsSinceLastScan": 420
+            }
+          ]
+        }
+      ],
+      "expTotalRiskLevel": 2,
+      "expTotalMinimumDistinctEncountersWithLowRisk": 0,
+      "expAgeOfMostRecentDateWithLowRisk": null,
+      "expAgeOfMostRecentDateWithHighRisk": 1,
+      "expTotalMinimumDistinctEncountersWithHighRisk": 1
+    },
+    {
+      "description": "count Exposure Windows with same Date/TRL but different CallibrationConfidence separately towards distinct encounters with High Risk",
+      "exposureWindows": [
+        {
+          "ageInDays": 1,
+          "reportType": 4,
+          "infectiousness": 2,
+          "calibrationConfidence": 0,
+          "scanInstances": [
+            {
+              "typicalAttenuation": 30,
+              "minAttenuation": 25,
+              "secondsSinceLastScan": 420
+            },
+            {
+              "typicalAttenuation": 30,
+              "minAttenuation": 25,
+              "secondsSinceLastScan": 420
+            }
+          ]
+        },
+        {
+          "ageInDays": 1,
+          "reportType": 4,
+          "infectiousness": 2,
+          "calibrationConfidence": 1,
+          "scanInstances": [
+            {
+              "typicalAttenuation": 30,
+              "minAttenuation": 25,
+              "secondsSinceLastScan": 420
+            },
+            {
+              "typicalAttenuation": 30,
+              "minAttenuation": 25,
+              "secondsSinceLastScan": 420
+            }
+          ]
+        }
+      ],
+      "expTotalRiskLevel": 2,
+      "expTotalMinimumDistinctEncountersWithLowRisk": 0,
+      "expAgeOfMostRecentDateWithLowRisk": null,
+      "expAgeOfMostRecentDateWithHighRisk": 1,
+      "expTotalMinimumDistinctEncountersWithHighRisk": 2
+    },
+    {
+      "description": "count Exposure Windows with same Date/CallibrationConfidence but different TRL separately towards distinct encounters with High Risk",
+      "exposureWindows": [
+        {
+          "ageInDays": 1,
+          "reportType": 4,
+          "infectiousness": 2,
+          "calibrationConfidence": 0,
+          "scanInstances": [
+            {
+              "typicalAttenuation": 30,
+              "minAttenuation": 25,
+              "secondsSinceLastScan": 420
+            },
+            {
+              "typicalAttenuation": 30,
+              "minAttenuation": 25,
+              "secondsSinceLastScan": 420
+            }
+          ]
+        },
+        {
+          "ageInDays": 1,
+          "reportType": 3,
+          "infectiousness": 2,
+          "calibrationConfidence": 0,
+          "scanInstances": [
+            {
+              "typicalAttenuation": 30,
+              "minAttenuation": 25,
+              "secondsSinceLastScan": 420
+            },
+            {
+              "typicalAttenuation": 30,
+              "minAttenuation": 25,
+              "secondsSinceLastScan": 420
+            }
+          ]
+        }
+      ],
+      "expTotalRiskLevel": 2,
+      "expTotalMinimumDistinctEncountersWithLowRisk": 0,
+      "expAgeOfMostRecentDateWithLowRisk": null,
+      "expAgeOfMostRecentDateWithHighRisk": 1,
+      "expTotalMinimumDistinctEncountersWithHighRisk": 2
+    },
+    {
+      "description": "count Exposure Windows with same TRL/CallibrationConfidence but different Date separately towards distinct encounters with High Risk",
+      "exposureWindows": [
+        {
+          "ageInDays": 1,
+          "reportType": 4,
+          "infectiousness": 2,
+          "calibrationConfidence": 0,
+          "scanInstances": [
+            {
+              "typicalAttenuation": 30,
+              "minAttenuation": 25,
+              "secondsSinceLastScan": 420
+            },
+            {
+              "typicalAttenuation": 30,
+              "minAttenuation": 25,
+              "secondsSinceLastScan": 420
+            }
+          ]
+        },
+        {
+          "ageInDays": 2,
+          "reportType": 4,
+          "infectiousness": 2,
+          "calibrationConfidence": 0,
+          "scanInstances": [
+            {
+              "typicalAttenuation": 30,
+              "minAttenuation": 25,
+              "secondsSinceLastScan": 420
+            },
+            {
+              "typicalAttenuation": 30,
+              "minAttenuation": 25,
+              "secondsSinceLastScan": 420
+            }
+          ]
+        }
+      ],
+      "expTotalRiskLevel": 2,
+      "expTotalMinimumDistinctEncountersWithLowRisk": 0,
+      "expAgeOfMostRecentDateWithLowRisk": null,
+      "expAgeOfMostRecentDateWithHighRisk": 1,
+      "expTotalMinimumDistinctEncountersWithHighRisk": 2
+    },
+    {
+      "description": "determine High Risk in total if there is at least one Exposure Window with High Risk",
+      "exposureWindows": [
+        {
+          "ageInDays": 2,
+          "reportType": 3,
+          "infectiousness": 1,
+          "calibrationConfidence": 0,
+          "scanInstances": [
+            {
+              "typicalAttenuation": 30,
+              "minAttenuation": 25,
+              "secondsSinceLastScan": 300
+            },
+            {
+              "typicalAttenuation": 30,
+              "minAttenuation": 25,
+              "secondsSinceLastScan": 300
+            }
+          ]
+        },
+        {
+          "ageInDays": 1,
+          "reportType": 4,
+          "infectiousness": 2,
+          "calibrationConfidence": 0,
+          "scanInstances": [
+            {
+              "typicalAttenuation": 30,
+              "minAttenuation": 25,
+              "secondsSinceLastScan": 420
+            },
+            {
+              "typicalAttenuation": 30,
+              "minAttenuation": 25,
+              "secondsSinceLastScan": 420
+            }
+          ]
+        }
+      ],
+      "expTotalRiskLevel": 2,
+      "expTotalMinimumDistinctEncountersWithLowRisk": 1,
+      "expAgeOfMostRecentDateWithLowRisk": 2,
+      "expAgeOfMostRecentDateWithHighRisk": 1,
+      "expTotalMinimumDistinctEncountersWithHighRisk": 1
+    },
+    {
+      "description": "handle empty set of Exposure Windows",
+      "exposureWindows": [],
+      "expTotalRiskLevel": 1,
+      "expTotalMinimumDistinctEncountersWithLowRisk": 0,
+      "expTotalMinimumDistinctEncountersWithHighRisk": 0,
+      "expAgeOfMostRecentDateWithLowRisk": null,
+      "expAgeOfMostRecentDateWithHighRisk": null
+    },
+    {
+      "description": "handle empty set of Scan Instances (should never happen)",
+      "exposureWindows": [
+        {
+          "ageInDays": 1,
+          "reportType": 2,
+          "infectiousness": 2,
+          "calibrationConfidence": 0,
+          "scanInstances": []
+        }
+      ],
+      "expTotalRiskLevel": 1,
+      "expTotalMinimumDistinctEncountersWithLowRisk": 0,
+      "expTotalMinimumDistinctEncountersWithHighRisk": 0,
+      "expAgeOfMostRecentDateWithLowRisk": null,
+      "expAgeOfMostRecentDateWithHighRisk": null
+    },
+    {
+      "description": "handle a typicalAttenuation of zero (should never happen)",
+      "exposureWindows": [
+        {
+          "ageInDays": 1,
+          "reportType": 3,
+          "infectiousness": 2,
+          "calibrationConfidence": 0,
+          "scanInstances": [
+            {
+              "typicalAttenuation": 0,
+              "minAttenuation": 25,
+              "secondsSinceLastScan": 300
+            },
+            {
+              "typicalAttenuation": 70,
+              "minAttenuation": 25,
+              "secondsSinceLastScan": 300
+            }
+          ]
+        }
+      ],
+      "expTotalRiskLevel": 1,
+      "expTotalMinimumDistinctEncountersWithLowRisk": 1,
+      "expTotalMinimumDistinctEncountersWithHighRisk": 0,
+      "expAgeOfMostRecentDateWithLowRisk": 1,
+      "expAgeOfMostRecentDateWithHighRisk": null,
+      "expNumberOfExposureWindowsWithLowRisk": 1,
+      "expNumberOfExposureWindowsWithHighRisk": 0
+    },
+    {
+      "description": "handle secondsSinceLastScan of zero (should never happen)",
+      "exposureWindows": [
+        {
+          "ageInDays": 1,
+          "reportType": 3,
+          "infectiousness": 2,
+          "calibrationConfidence": 0,
+          "scanInstances": [
+            {
+              "typicalAttenuation": 70,
+              "minAttenuation": 25,
+              "secondsSinceLastScan": 0
+            },
+            {
+              "typicalAttenuation": 70,
+              "minAttenuation": 25,
+              "secondsSinceLastScan": 300
+            },
+            {
+              "typicalAttenuation": 70,
+              "minAttenuation": 25,
+              "secondsSinceLastScan": 300
+            }
+          ]
+        }
+      ],
+      "expTotalRiskLevel": 1,
+      "expTotalMinimumDistinctEncountersWithLowRisk": 1,
+      "expTotalMinimumDistinctEncountersWithHighRisk": 0,
+      "expAgeOfMostRecentDateWithLowRisk": 1,
+      "expAgeOfMostRecentDateWithHighRisk": null,
+      "expNumberOfExposureWindowsWithLowRisk": 1,
+      "expNumberOfExposureWindowsWithHighRisk": 0
+    }
+  ]
+}
-- 
GitLab