import java.util.regex.Pattern
import com.gradle.develocity.agent.gradle.test.*
import com.gradle.develocity.agent.gradle.internal.test.*

enum TestCategories {
    ActiveDirectory(false, true),
    ActuatorEndpoint(true, true),
    AmazonWebServices(true, true),
    AMQP(false, true),
    Attributes(true, true),
    AttributeRelease(true, true),
    AttributeRepository(true, true),
    Audits(true, true),
    Authentication(true, true),
    AuthenticationHandler(true, true),
    AuthenticationPolicy(true, true),
    AuthenticationMetadata(true, true),
    AuthenticationThrottling(false, true),
    Authorization(true, true),
    Azure(false, true),
    CAS(true, true),
    CasConfiguration(true, true),
    Cassandra(false, true),
    Cipher(true, true),
    Consent(true, true),
    Cookie(true, true),
    Delegation(true, true),
    DuoSecurity(true, true),
    DynamoDb(false, true),
    Elastic(true, true),
    ExpirationPolicy(true),
    Events(true, true),
    FileSystem(false, true),
    GCP(false, true),
    GeoLocation(true, true),
    Git(true, true),
    Groovy(true, true),
    GroovyAuthentication(true, true),
    GroovyMfa(true, true),
    GroovyServices(true, true),
    Grouper(true, true),
    Hazelcast(false, true),
    Hibernate(false, true),
    Ignite(false, true),
    Impersonation(true, true),
    InfluxDb(true, true),
    JDBC(false, true),
    JDBCMFA(false, true),
    JDBCAuthentication(false, true),
    JMX(true, true),
    Kafka(true, true),
    LdapAttributes(true, true),
    LdapAuthentication(false, true),
    Ldap(false, true),
    LdapServices(false, true),
    LdapRepository(false, true),
    Logout(true, true),
    Mail(true, true),
    MariaDb(false, true),
    Memcached(false, true),
    Metrics(true, true),
    MFA(true, true),
    MFAProvider(true, true),
    MFATrigger(true, true),
    MFATrustedDevices(false, true),
    MongoDb(false, true),
    MongoDbMFA(false, true),
    MsSqlServer(false, true),
    MySQL(false, true),
    Native(true, true),
    OAuth(true, true),
    OAuthToken(true, true),
    OAuthWeb(false, true),
    OIDC(true, true),
    OIDCWeb(true, true),
    OIDCAttributes(true, true),
    OIDCAuthentication(true, true),
    OIDCServices(true, true),
    Oracle(false, true),
    PasswordOps(true, true),
    Postgres(false, true),
    Radius(true, true),
    Redis(false, true),
    RegisteredService(true, true),
    RestfulApi(false, true),
    RestfulApiAuthentication(true, true),
    SAML(true, true),
    SAML1(true, true),
    SAML2(false, true),
    SAML2Web(false, true),
    SAMLMetadata(true, true),
    SAMLServiceProvider(true, true),
    SAMLLogout(true, true),
    SAMLAttributes(true, true),
    SAMLResponse(false, true),
    SCIM(true, true),
    SHELL(true, true),
    Simple(true, true),
    SMS(true, true),
    Spnego(true, true),
    Syncope(true, true),
    Tickets(true, true),
    UMA(true, true),
    Utility(true, true),
    Web(true, true),
    WebApp(true, true),
    Webflow(true, true),
    WebflowActions(true, true),
    WebflowAccountActions(true, true),
    WebflowServiceActions(true, true),
    WebflowAuthenticationActions(true, true),
    WebflowConfig(true, true),
    WebflowMfaConfig(true, true),
    WebflowEvents(true, true),
    WebflowMfaActions(true),
    WSFederation(true, true),
    X509(true, true),
    ZooKeeper(true, true)

    boolean parallelEnabled
    private ptsEnabled
    
    TestCategories() {
        parallelEnabled = false
        ptsEnabled = false
    }

    TestCategories(boolean parallelEnabled) {
        this.parallelEnabled = parallelEnabled
    }

    TestCategories(boolean parallelEnabled, boolean ptsEnabled) {
        this.parallelEnabled = parallelEnabled
        this.ptsEnabled = ptsEnabled
    }
}

if (rootProject.tasks.findByName("testCategories") == null) {
    rootProject.tasks.register("testCategories") {
        description = "Display all available test categories"
        doLast {
            def pattern = System.getenv()["CATEGORY_REGEX"] ?: ".*"
            def categoryMatchPattern = Pattern.compile(pattern, Pattern.CASE_INSENSITIVE)
            def result = TestCategories.values()
                    .findAll { it.name().find(categoryMatchPattern) }
                    .toSorted({ o1, o2 ->
                        (o1.name() <=> o2.name())
                    })
                    .collect { it.name().toLowerCase(Locale.ENGLISH) }
            println(groovy.json.JsonOutput.toJson(result))
        }
    }
}

if (rootProject.tasks.findByName("puppeteerScenarios") == null) {
    rootProject.tasks.register("puppeteerScenarios") {
        description = "Display all available puppeteer scenarios"

        def puppeteerScenariosFrom = project.providers.gradleProperty("puppeteerScenariosFrom").getOrNull()
        def puppeteerScenariosTo = project.providers.gradleProperty("puppeteerScenariosTo").getOrNull()
        def scenariosDirectory = rootProject.file("ci/tests/puppeteer/scenarios")

        doLast {
            def scenarios = []
            def pattern = System.getenv()["SCENARIO_REGEX"] ?: ".*"
            def scenarioMatchPattern = Pattern.compile(pattern, Pattern.CASE_INSENSITIVE)

            scenariosDirectory.eachDirMatch(scenarioMatchPattern) {
                scenarios += it.getName()
            }
            Collections.sort(scenarios)
            if (puppeteerScenariosFrom != null && puppeteerScenariosTo != null) {
                def fromScenario = Integer.parseInt(puppeteerScenariosFrom)
                def toScenario = Integer.parseInt(puppeteerScenariosTo)
                if (toScenario > scenarios.size())
                    toScenario = scenarios.size()
                scenarios = scenarios.subList(fromScenario, toScenario)
            }
            println(groovy.json.JsonOutput.toJson(scenarios))
        }
    }
}

TestCategories.values().each({ testCategory ->
    def sourceTestDirs = project.sourceSets.test.java.srcDirs
    if (hasTestsOfCategory(sourceTestDirs, testCategory.name())) {
        def taskName = "test${testCategory.name()}"
        if (!tasks.getNames().contains(taskName)) {
            def jacocoTestExecutionResultsFile = project.layout.buildDirectory.file("jacoco/jacoco-${taskName}.exec").get().asFile
            def shouldWeTest = shouldTest(sourceTestDirs, testCategory.name())

            tasks.register(taskName, Test) {
                description = "Run unit/integration tests to verify the test category: ${testCategory.name()}"
                group = "CAS Tests"
                testClassesDirs = sourceSets.test.output.classesDirs
                classpath = sourceSets.test.runtimeClasspath
                enabled = shouldWeTest
                outputs.file(jacocoTestExecutionResultsFile)

                useJUnitPlatform {
                    includeTags testCategory.name()
                }

                def ptsEnabled = shouldWeTest && testCategory.ptsEnabled && Boolean.valueOf(System.getProperty("PTS_ENABLED") ?: "true") && !rootProject.publishReleases
                develocity.predictiveTestSelection {
                    enabled = ptsEnabled
                    // Enable PTS with code coverage support
                    this as PredictiveTestSelectionConfigurationInternal
                    mergeCodeCoverage = true
                }
                logger.info "Predictive test selection for ${project.name} with task ${taskName} is ${ptsEnabled ? 'enabled' : 'disabled'}."

                systemProperty("junit.platform.discovery.issue.severity.critical", "info")
                
                if (testCategory.parallelEnabled) {
                    systemProperty("junit.jupiter.execution.parallel.enabled", true)
                    systemProperty("junit.jupiter.execution.parallel.mode.default", "concurrent")
                    systemProperty("junit.jupiter.execution.parallel.mode.classes.default", "concurrent")
                    systemProperty("junit.jupiter.execution.parallel.config.strategy", "dynamic")
                    systemProperty("junit.jupiter.execution.parallel.config.dynamic.factor", "8")
                }

                if (!project.path.contains(":docs")) {
                    jacoco {
                        dumpOnExit = true
                        excludes = rootProject.excludedFilesFromTestCoverage
                        destinationFile = jacocoTestExecutionResultsFile
                        classDumpDir = layout.buildDirectory.dir('jacoco/classpathdumps').get().asFile
                    }
                }
                jvmArgs += "-javaagent:${configurations.jvmAgents.asPath}"
            }
        }
    }
})

tasks.named('test') {
    enabled = false
    onlyIf = {
        false
    }
}

tasks.withType(Test).configureEach {
    scanForTestClasses = false
    ignoreFailures = providers.systemProperty("ignoreTestFailures").present
    forkEvery = 2000
    maxParallelForks = 8
    
    failFast = true

    def list = []
    list.add("--add-modules")
    list.add("java.se")

    list.add("--add-exports")
    list.add("java.base/jdk.internal.ref=ALL-UNNAMED")
    list.add("--add-exports")
    list.add("java.base/jdk.internal.misc=ALL-UNNAMED")
    list.add("--add-exports")
    list.add("java.xml.crypto/org.jcp.xml.dsig.internal.dom=ALL-UNNAMED")
    list.add("--add-exports")
    list.add("java.base/sun.nio.ch=ALL-UNNAMED")
    list.add("--add-exports")
    list.add("java.base/java.nio=ALL-UNNAMED")
    list.add("--add-exports")
    list.add("java.management/com.sun.jmx.mbeanserver=ALL-UNNAMED")
    list.add("--add-exports")
    list.add("jdk.internal.jvmstat/sun.jvmstat.monitor=ALL-UNNAMED")
    list.add("--add-exports")
    list.add("java.base/sun.reflect.generics.reflectiveObjects=ALL-UNNAMED")

    list.add("--add-opens")
    list.add("java.base/java.lang=ALL-UNNAMED")
    list.add("--add-opens")
    list.add("java.base/java.util=ALL-UNNAMED")
    list.add("--add-opens")
    list.add("java.base/java.time=ALL-UNNAMED")
    list.add("--add-opens")
    list.add("java.base/java.util.concurrent=ALL-UNNAMED")
    list.add("--add-opens")
    list.add("java.base/java.util.concurrent.locks=ALL-UNNAMED")
    list.add("--add-opens")
    list.add("java.base/java.net=ALL-UNNAMED")
    list.add("--add-opens")
    list.add("java.base/java.nio=ALL-UNNAMED")
    list.add("--add-opens")
    list.add("java.base/sun.nio.ch=ALL-UNNAMED")
    list.add("--add-opens")
    list.add("java.management/sun.management=ALL-UNNAMED")
    list.add("--add-opens")
    list.add("jdk.management/com.sun.management.internal=ALL-UNNAMED")
    list.add("--add-opens")
    list.add("java.base/sun.security.action=jdk.crypto.ec")

    list.add("-Xms512m")
    list.add("-Xmx6g")

    list.add("-XX:+EnableDynamicAgentLoading")
    jvmArgs = list

    testLogging {
        def testLoggingEvents = providers.systemProperty("testLoggingEvents")
                .orElse("PASSED,SKIPPED,FAILED,STANDARD_OUT,STANDARD_ERROR").get().toUpperCase().split(",")
        events testLoggingEvents
        exceptionFormat = "full"

        def verbose = providers.systemProperty("verbose").present

        showStandardStreams = verbose && testLoggingEvents.contains("STANDARD_OUT")
        showExceptions = verbose && testLoggingEvents.contains("FAILED")
        showCauses = verbose
    }
    
    def skipRetry = providers.systemProperty('skipTestRetry').present
    if (!skipRetry) {
        develocity.testRetry {
            maxRetries = 3
            maxFailures = 5
            failOnPassedAfterRetry = false
        }
    }

    filter {
        setFailOnNoMatchingTests(false)
        excludeTestsMatching "*TestsSuite"
    }
}

def shouldTest(Set sourceTestDirectories, String category) {
    def foundTest = gradle.startParameter.taskNames.any { task -> task.matches(/^test${category}$/) }
    if (!foundTest) {
        logger.debug "No tests with category ${category} found"
    }
    return !providers.systemProperty("skipTests").present && foundTest && hasTestsOfCategory(sourceTestDirectories, category)
}

def hasTestsOfCategory(Set sourceTestDirectories, String category) {
    def res = srcTagMap.computeIfAbsent(sourceTestDirectories) {
        def tags = new HashSet()
        sourceTestDirectories.each { src ->
            try {
                src.eachFileRecurse { file ->
                    if (file ==~ /.*Tests\.\w+/) {
                        file.readLines().parallelStream().forEach() { line ->
                            def match = line =~ /^\s*@Tag\("(\w+)"\)\s*$/
                            if (match) {
                                tags.addAll(match.collect { it[1] })
                            }
                        }
                    }
                }
            } catch (ignored) {
            }
        }
        tags
    }
    res.contains(category)
}

