import java.util.regex.Matcher
import java.util.regex.Pattern

/*
 * testing.gradle
 *
 * This source file is part of the FoundationDB open source project
 *
 * Copyright 2015-2018 Apple Inc. and the FoundationDB project authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

apply plugin: 'jacoco'
// configure for test, other *Test are configured below
jacoco {
    toolVersion = libs.versions.jacoco.get()
}

test {
    useJUnitPlatform {
        excludeTags 'WipesFDB'
        excludeTags 'AutomatedTest'
    }
}

task destructiveTest(type: Test) {
    useJUnitPlatform {
        includeTags 'WipesFDB'
        maxParallelForks = 1
    }
    reports {
        junitXml.outputLocation = file("${buildDir}/test-results/destructive")
    }
}

task performanceTest(type: Test) {
    useJUnitPlatform {
        includeTags 'Performance'
        enableAssertions = false
    }
}

task quickTest(type: Test) {
    systemProperties["tests.runQuick"] = true
    useJUnitPlatform {
        excludeTags 'WipesFDB'
    }
}

// Tests that run similarly to quickTest, but with an embedded RPC server instead of embedded connection
task rpcTest(type: Test) {
    systemProperties["tests.runRPC"] = true
    useJUnitPlatform {
        excludeTags 'WipesFDB'
    }
}

// Tests that run a single version on multiple external servers
task singleVersionTest(type: Test) {
    systemProperties["tests.singleVersion"] = true
    useJUnitPlatform {
        excludeTags 'WipesFDB'
    }
}

def getMixedModeVersion(descriptor) {
    while (descriptor != null) {
        // don't display this or any of the parents. Let's assume that nobody ever
        // sets the display name in code to start with "Gradle Test Executor".
        // it appears to be suffixed with a number, but I didn't investigate why
        if (descriptor.displayName.startsWith("Gradle Test Executor")) {
            break
        }

        Matcher versionMatch = descriptor.displayName =~ /^MultiServer \((?:((?:\d+\.)+\d+) then JDBC In-Process|JDBC In-Process then ((?:\d+\.)+\d+))\)/
        if (versionMatch.size() != 0) {
            def version = versionMatch[0][1]
            if (version == null) {
                version = versionMatch[0][2]
            }
            return version
        }
        descriptor = descriptor.parent
    }
    return null
}

task mixedModeTest(type: Test) {
    useJUnitPlatform {
        includeTags 'MixedMode'
    }

    ignoreFailures = true

    // Skip non-MultiServer yaml tests
    systemProperties['tests.mixedModeOnly'] = 'true'

    def markdownReports = new File("$rootDir/.out/reports/")
    def mixedModeResults = new File(markdownReports, "mixed-mode-results.log")
    doFirst {
        // This may have issues with running tasks concurrently
        if (!markdownReports.exists()) {
            markdownReports.mkdirs()
        }
    }
    afterTest { descriptor, result ->
        def version = getMixedModeVersion(descriptor)
        if (version == null) {
            throw new RuntimeException("Could not find version " + getFullDisplayName(descriptor))
        } else {
            mixedModeResults.append("${result.resultType} ${version}\n")
        }
    }
}

def getFullDisplayName(descriptor) {
    def fullName = descriptor.displayName
    descriptor = descriptor.parent
    while (descriptor != null) {
        // don't display this or any of the parents. Let's assume that nobody ever
        // sets the display name in code to start with "Gradle Test Executor".
        // it appears to be suffixed with a number, but I didn't investigate why
        if (descriptor.displayName.startsWith("Gradle Test Executor")) {
            break
        }
        def openParen = descriptor.displayName.indexOf("(")
        // in the case where someone sets the display name to include the method name, it's best
        // to skip the method name itself, e.g.:
        // LuceneIndexMaintenanceTest > randomizedRepartitionTest(boolean, boolean, boolean, int, int, int, long) > randomizedRepartitionTest(true, false, false, 13, 3, 20, 9237590782644) STARTED
        // LuceneIndexMaintenanceTest > randomizedRepartitionTest(boolean, boolean, boolean, int, int, int, long) > randomizedRepartitionTest(true, false, false, 13, 3, 20, 9237590782644) SUCCESS (1985ms)
        if (openParen < 0 || !fullName.startsWith(descriptor.displayName.substring(0, openParen))) {
            fullName = "${descriptor.displayName}" + " > " + fullName
        }
        descriptor = descriptor.parent
    }
    return fullName
}

def configureTestTask = { propertyPrefix, task ->
    def handled = [propertyPrefix + '.ignoreFailures',
                   propertyPrefix + '.debug',
                   propertyPrefix + '.single',
                   propertyPrefix + '.maxParallelForks',
                   propertyPrefix + '.exclude',
                   propertyPrefix + '.forkEvery']
    System.properties.each { prop ->
        def prefix = "${propertyPrefix}.sysProp."
        if (!prop.key.startsWith(prefix.toString())) {
            return
        };
        def setkey = prop.key.substring(prefix.length())
        task.systemProperties[setkey] = prop.value
        logger.debug "Set system property ${setkey} = ${prop.value} on ${propertyPrefix}"
        handled.add(prop.key)
    }

    def tmpdir = file("$buildDir/tmp-${task.name}")
    task.doFirst {
        delete tmpdir
        tmpdir.mkdirs()
    }
    task.systemProperties['java.io.tmpdir'] = tmpdir.absolutePath
    task.systemProperties['junit.jupiter.extensions.autodetection.enabled'] = 'true'

    // configure test logging
    task.systemProperties['log4j2.configurationFile'] = rootProject.file('gradle/scripts/log4j-test.properties').toURI()
    task.systemProperties['mme.app.useLog4jPropertyConfigurator'] = 'false'

    task.maxParallelForks = Integer.getInteger(propertyPrefix + '.maxParallelForks', 1)
    task.forkEvery = Integer.getInteger(propertyPrefix + '.forkEvery', 0)

    task.debug = Boolean.getBoolean(propertyPrefix + '.debug')
    task.ignoreFailures = Boolean.getBoolean(propertyPrefix + '.ignoreFailures')

    def excludes = System.getProperty(propertyPrefix + '.exclude')
    if (excludes != null) {
        task.exclude excludes
    }

    if (task.name != 'performanceTest') {
        // Configure whether or not tests will validate that asyncToSync isn't being called in async
        // context.  See BlockingInAsyncDetection class for details on values.
        // disable BLOCKING_DETECTION to better simulate performance
        task.systemProperties["com.apple.foundationdb.record.blockingInAsyncDetection"] =
            System.getenv('BLOCKING_DETECTION') ?: "IGNORE_COMPLETE_EXCEPTION_BLOCKING"
    }

    def reportsDirProperty = "${task.name}.reportsDir".toString()
    handled.add(reportsDirProperty)
    def reportsDir = System.getProperty(reportsDirProperty)
    if (reportsDir != null) {
        task.reports.junitXml.destination = reportsDir
    }

    System.properties.each { prop ->
        if (prop.key.startsWith(propertyPrefix + '.') && !handled.contains(prop.key)) {
            logger.warn "Unknown system property that looks like it was intended to configure this task: ${prop.key}=${prop.value}"
        }
    }
}

ext.skippedStackTraceEntries = Pattern.compile(/(?s)/ +
                                               /(?<indent>\s+)at java\.base\/jdk\.internal\.reflect\.NativeMethodAccessorImpl\.invoke0\(Native Method\)/ +
                                               /\s+at java\.base\/jdk\.internal\.reflect\.NativeMethodAccessorImpl\.invoke\(NativeMethodAccessorImpl\.java:\d+\)/ +
                                               /\s+at java\.base\/jdk\.internal\.reflect\.DelegatingMethodAccessorImpl\.invoke\(DelegatingMethodAccessorImpl\.java:\d+\)/ +
                                               /\s+at java\.base\/java\.lang\.reflect\.Method\.invoke\(Method\.java:\d+\)/ +
                                               /\s+at org\.junit\.platform\.commons\.util\.ReflectionUtils\.invokeMethod\(ReflectionUtils\.java:\d+\).*?/ +
                                               /(?=\s+Suppressed:|\s+Caused by:|$)/)


def betterException(String exception) {
    return skippedStackTraceEntries.matcher(exception).replaceAll("\n\${indent}... junit stack ...")
}

tasks.withType(Test) { theTask ->
    configureTestTask('allTest', theTask)
    configureTestTask(theTask.name, theTask)
    testLogging {
        events 'failed'
        exceptionFormat = 'full'
    }
    // This creates two reports to be picked up by the gradle actions (see actions/gradle-test/action.yml)
    // We initialize the files here, to be filled out by test listeners below
    // summary has a summary of all the test tasks we ran, and the success/skipped/failure counts & duration
    // failures includes any failures that happened, with the stack trace
    def markdownReports = new File("$rootDir/.out/reports/")
    def summary = new File(markdownReports, "test-summary.md")
    def failures = new File(markdownReports, "test-failures.md")
    doFirst {
        // This may have issues with running tasks concurrently
        if (!markdownReports.exists()) {
            markdownReports.mkdirs()
        }
        if (!summary.exists()) {
            summary.append("|Test run|❌|⚠️|✅|time|\n")
            summary.append("|-|-|-|-|-|\n")
        }
    }

    beforeTest { descriptor -> 
        println "${Instant.now()} ${getFullDisplayName(descriptor)} STARTED"
    } 
    afterTest { descriptor, result ->
        def duration = String.format(Locale.ROOT, "%,d", result.endTime - result.startTime)
        println "${Instant.now()} ${getFullDisplayName(descriptor)} ${result.resultType} (${duration}ms)"
        println()
        if (result.resultType == TestResult.ResultType.FAILURE) {
            failures.append(
                "<details>\n" +
                    "<summary> ❌ ${getFullDisplayName(descriptor)}</summary>\n" +
                    "(${duration}ms)\n\n" + // blank line is required for ``` to work
                    "```\n${betterException(result.getException().asString())}\n```\n" +
                "</details>\n")
        }
    }
    afterSuite { descriptor, result ->
        // Very few subprojects acutally have destructive tests, so strip those out of the summary.
        // We leave the ones that have nothing under test, because that would be suspicious if a subproject had
        // no tests
        if (descriptor.parent == null && !(descriptor.toString().endsWith(":destructiveTest") && result.getTestCount() == 0)) {
            def duration = String.format(Locale.ROOT, "%,d", result.endTime - result.startTime)
            def description = descriptor.toString().replaceFirst("Gradle Test Run ", "")
            summary.append(
                "|${description}|${result.getFailedTestCount()}|${result.getSkippedTestCount()}|${result.getSuccessfulTestCount()}|${duration}ms|\n")
        }
    }
    reports {
        junitXml.outputPerTestCase = true
    }
}

tasks.withType(Test).configureEach { task ->
    if (task.name != 'performanceTest') {
        // Configure whether or not tests will validate that asyncToSync isn't being called in async
        // context.  See BlockingInAsyncDetection class for details on values.
        // disable BLOCKING_DETECTION to better simulate performance
        task.systemProperties["com.apple.foundationdb.record.blockingInAsyncDetection"] =
                System.getenv('BLOCKING_DETECTION') ?: "IGNORE_COMPLETE_EXCEPTION_BLOCKING"

        // setup tag inclusion/exclusion
        if (project.hasProperty('tests.skipFDB')) {
            task.testFramework.options.excludeTags.add('RequiresFDB')
        }
        if (project.hasProperty('tests.skipSlow')) {
            task.testFramework.options.excludeTags.add('Slow')
            systemProperties["junit.jupiter.execution.timeout.default"] = "2s"
        } else {
            systemProperties["junit.jupiter.execution.timeout.default"] = "5m"
        }
        if (project.hasProperty('tests.nightly')) {
            systemProperties['tests.nightly'] = 'true'
        } else {
            task.testFramework.options.excludeTags.add('SuperSlow')
        }
        if (!project.hasProperty('tests.includeRandom')) {
            task.testFramework.options.excludeTags.add('Random')
        } else {
            if (!project.hasProperty('randomTestsLocale')) {
                def random = new Random()
                def availableLocales = Locale.getAvailableLocales()
                rootProject.ext.randomTestsLocale = availableLocales[random.nextInt(availableLocales.size())]
                logger.warn("Running tests with locale: ${rootProject.ext.randomTestsLocale}")
            }
            task.systemProperty('user.language', rootProject.ext.randomTestsLocale.language)
            task.systemProperty('user.country', rootProject.ext.randomTestsLocale.country)
            systemProperties['tests.includeRandom'] = 'true'
        }
        if (project.hasProperty('tests.iterations')) {
            systemProperties['tests.iterations'] = project.getProperty('tests.iterations')
        }
        task.testFramework.options.excludeTags.add('Performance')

        if (task.name == 'test') {
            task.systemProperty('junit.jupiter.execution.parallel.enabled', 'false')
            task.systemProperty('junit.jupiter.execution.parallel.mode.default', 'same_thread')
            task.systemProperty('junit.jupiter.execution.parallel.mode.classes.default', 'concurrent')
        }
    }

}

task testJar(type: Jar, dependsOn: testClasses) {
    group = 'Build'
    description = 'Build a jar file of test classes as an exported artifact'
    archiveClassifier = 'test'
    from sourceSets.test.output
}

configurations {
    tests {
        extendsFrom testRuntimeOnly
    }
}

artifacts {
    tests testJar
}

task quickCheck {
    group = 'Verification'
    description = 'Perform fast build verification'

    dependsOn testClasses
}
