plugins {
    id 'java'
    id 'org.springframework.boot' version '3.4.1'
    id 'io.spring.dependency-management' version '1.1.7'        // manages implicit dependencies
    id 'io.openapiprocessor.openapi-processor' version '2023.2' // generates Controller-interface and resources from API-spec
    id 'com.github.jk1.dependency-license-report' version '2.9' // checks dependency-license compatibility
    id "org.owasp.dependencycheck" version "12.0.1"             // checks dependencies for known vulnerabilities
    id "com.diffplug.spotless" version "7.0.2"                  // formats + checks formatting for source-code
    id 'jacoco'                                                 // determines code-coverage of tests
    id 'info.solidsoft.pitest' version '1.15.0'                 // performs mutation testing
    id 'se.patrikerdes.use-latest-versions' version '0.2.18'    // updates module and plugin versions
    id 'com.github.ben-manes.versions' version '0.52.0'         // determines which dependencies have updates
}

// HOWTO: find out which dependency versions are managed by Spring Boot:
//  https://docs.spring.io/spring-boot/appendix/dependency-versions/coordinates.html

group = 'net.hostsharing'
version = '0.0.1-SNAPSHOT'

wrapper {
    distributionType = Wrapper.DistributionType.BIN
    gradleVersion = '8.5'
}

// TODO.impl: self-attaching is deprecated, see:
//  https://javadoc.io/doc/org.mockito/mockito-core/latest/org/mockito/Mockito.html#0.3

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
    testCompile {
        extendsFrom testAnnotationProcessor

        // Only JUNit 5 (Jupiter) should be used at compile time.
        // For runtime it's still needed by testcontainers, though.
        exclude group: 'junit', module: 'junit'
        exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
    }
}

repositories {
    mavenCentral()
    maven { url 'https://repo.spring.io/milestone' }
    maven { url 'https://repo.spring.io/snapshot' }
}

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(21)
        vendor = JvmVendorSpec.ADOPTIUM
        implementation = JvmImplementation.VENDOR_SPECIFIC
    }
}

ext {
    set('testcontainersVersion', "1.17.3")
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-data-rest'
    implementation 'org.springframework.boot:spring-boot-starter-jdbc'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-validation'
    implementation 'org.springframework.boot:spring-boot-starter-actuator'
    implementation 'org.springframework.boot:spring-boot-starter-security'
    implementation 'com.github.gavlyukovskiy:datasource-proxy-spring-boot-starter:1.10.0'
    implementation 'org.springdoc:springdoc-openapi:2.8.3'
    implementation 'org.postgresql:postgresql'
    implementation 'org.liquibase:liquibase-core'
    implementation 'io.hypersistence:hypersistence-utils-hibernate-63:3.9.0'
    implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310'
    implementation 'org.openapitools:jackson-databind-nullable:0.2.6'
    implementation 'org.apache.commons:commons-text:1.13.0'
    implementation 'net.java.dev.jna:jna:5.16.0'
    implementation 'org.modelmapper:modelmapper:3.2.2'
    implementation 'org.iban4j:iban4j:3.2.10-RELEASE'
    implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.3'
    implementation 'org.reflections:reflections:0.10.2'

    compileOnly 'org.projectlombok:lombok'
    testCompileOnly 'org.projectlombok:lombok'

    // TODO.impl: version conflict with SpringDoc, check later and re-enable if fixed
    // developmentOnly 'org.springframework.boot:spring-boot-devtools'

    annotationProcessor 'org.projectlombok:lombok'
    testAnnotationProcessor 'org.projectlombok:lombok'

    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation 'org.testcontainers:testcontainers'
    testImplementation 'org.testcontainers:junit-jupiter'
    testImplementation 'org.junit.jupiter:junit-jupiter'
    testImplementation 'org.testcontainers:postgresql'
    testImplementation 'com.tngtech.archunit:archunit-junit5:1.3.0'
    testImplementation 'io.rest-assured:spring-mock-mvc'
    testImplementation 'org.hamcrest:hamcrest-core'
    testImplementation 'org.pitest:pitest-junit5-plugin:1.2.1'
    testImplementation 'org.junit.jupiter:junit-jupiter-api'
    testImplementation 'org.wiremock:wiremock-standalone:3.10.0'
}

dependencyManagement {
    imports {
        mavenBom "org.testcontainers:testcontainers-bom:${testcontainersVersion}"
    }
}

// Java Compiler Options
tasks.withType(JavaCompile) {
    options.compilerArgs += [
            "-parameters" // keep parameter names => no need for @Param for SpringData
    ]
}

// Configure tests
tasks.named('test') {
    useJUnitPlatform()
    jvmArgs '-Duser.language=en'
    jvmArgs '-Duser.country=US'
}

// OpenAPI Source Code Generation
openapiProcessor {
    springRoot {
        processorName 'spring'
        processor 'io.openapiprocessor:openapi-processor-spring:2022.5'
        apiPath "$projectDir/src/main/resources/api-definition/api-definition.yaml"
        mapping "$projectDir/src/main/resources/api-definition/api-mappings.yaml"
        targetDir "$buildDir/generated/sources/openapi-javax"
        showWarnings true
        openApiNullable true
    }
    springRbac {
        processorName 'spring'
        processor 'io.openapiprocessor:openapi-processor-spring:2022.5'
        apiPath "$projectDir/src/main/resources/api-definition/rbac/rbac.yaml"
        mapping "$projectDir/src/main/resources/api-definition/rbac/api-mappings.yaml"
        targetDir "$buildDir/generated/sources/openapi-javax"
        showWarnings true
        openApiNullable true
    }
    springTest {
        processorName 'spring'
        processor 'io.openapiprocessor:openapi-processor-spring:2022.5'
        apiPath "$projectDir/src/main/resources/api-definition/test/test.yaml"
        mapping "$projectDir/src/main/resources/api-definition/test/api-mappings.yaml"
        targetDir "$buildDir/generated/sources/openapi-javax"
        showWarnings true
        openApiNullable true
    }
    springHsOffice {
        processorName 'spring'
        processor 'io.openapiprocessor:openapi-processor-spring:2022.5'
        apiPath "$projectDir/src/main/resources/api-definition/hs-office/hs-office.yaml"
        mapping "$projectDir/src/main/resources/api-definition/hs-office/api-mappings.yaml"
        targetDir "$buildDir/generated/sources/openapi-javax"
        showWarnings true
        openApiNullable true
    }
    springHsBooking {
        processorName 'spring'
        processor 'io.openapiprocessor:openapi-processor-spring:2022.5'
        apiPath "$projectDir/src/main/resources/api-definition/hs-booking/hs-booking.yaml"
        mapping "$projectDir/src/main/resources/api-definition/hs-booking/api-mappings.yaml"
        targetDir "$buildDir/generated/sources/openapi-javax"
        showWarnings true
        openApiNullable true
    }
    springHsHosting {
        processorName 'spring'
        processor 'io.openapiprocessor:openapi-processor-spring:2022.5'
        apiPath "$projectDir/src/main/resources/api-definition/hs-hosting/hs-hosting.yaml"
        mapping "$projectDir/src/main/resources/api-definition/hs-hosting/api-mappings.yaml"
        targetDir "$buildDir/generated/sources/openapi-javax"
        showWarnings true
        openApiNullable true
    }
}
sourceSets.main.java.srcDir 'build/generated/sources/openapi'

abstract class ProcessSpring extends DefaultTask {}

tasks.register('processSpring', ProcessSpring)
['processSpringRoot',
 'processSpringRbac',
 'processSpringTest',
 'processSpringHsOffice',
 'processSpringHsBooking',
 'processSpringHsHosting'
].each {
    project.tasks.processSpring.dependsOn it
}
project.tasks.processResources.dependsOn processSpring
project.tasks.compileJava.dependsOn processSpring

// Rename javax to jakarta in OpenApi generated java files because
// io.openapiprocessor.openapi-processor 2022.5 does not yet support the openapiprocessor useSpringBoot3 config option.
// TODO.impl: Upgrade to io.openapiprocessor.openapi-processor >= 2024.2
//  and use either `bean-validation: true` in api-mapping.yaml or `useSpringBoot3 true` (not sure where exactly).
task openApiGenerate(type: Copy) {
    from "$buildDir/generated/sources/openapi-javax"
    into "$buildDir/generated/sources/openapi"
    filter { line -> line.replaceAll('javax', 'jakarta') }
}
compileJava.source "$buildDir/generated/sources/openapi"
compileJava.dependsOn openApiGenerate
openApiGenerate.dependsOn processSpring

// Spotless Code Formatting
spotless {
    java {
        removeUnusedImports()
        leadingTabsToSpaces(4)
        endWithNewline()
        toggleOffOn()

        target fileTree(rootDir) {
            include '**/*.java'
            exclude '**/generated/**/*.java'
        }
    }
}
project.tasks.check.dependsOn(spotlessCheck)
// HACK: no idea why spotless uses the output of these tasks, but we get warnings without those
project.tasks.spotlessJava.dependsOn(
        tasks.generateLicenseReport,
        // tasks.pitest, TODO.test: PiTest currently does not work, needs to be fixed
        tasks.jacocoTestReport,
        tasks.processResources,
        tasks.processTestResources)

// OWASP Dependency Security Test
dependencyCheck {
    nvd {
        apiKey = project.properties['OWASP_API_KEY'] // set it in ~/.gradle/gradle.properties
        delay = 16000
    }
    format = 'ALL'
    suppressionFile = 'etc/owasp-dependency-check-suppression.xml'
    failOnError = true
    failBuildOnCVSS = 5
}
project.tasks.check.dependsOn(dependencyCheckAnalyze)
project.tasks.dependencyCheckAnalyze.doFirst { // Why not doLast? See README.md!
    println "OWASP Dependency Security Report: file:///${project.rootDir}/build/reports/dependency-check-report.html"
}


// License Check
licenseReport {
    excludeBoms = true
    allowedLicensesFile = new File("$projectDir/etc/allowed-licenses.json")
}
project.tasks.check.dependsOn(checkLicense)

// HOWTO: run all tests except import- and scenario-tests: gw test
test {
    finalizedBy jacocoTestReport // generate report after tests
    excludes = [
            'net.hostsharing.hsadminng.**.generated.**',
    ]
    useJUnitPlatform {
        excludeTags 'importOfficeData', 'importHostingAssets', 'scenarioTest'
    }
}

// JaCoCo Test Code Coverage for unit-tests
jacoco {
    toolVersion = "0.8.10"
}
jacocoTestReport {
    dependsOn test
    afterEvaluate {
        classDirectories.setFrom(files(classDirectories.files.collect {
            fileTree(dir: it, exclude: [
                    "net/hostsharing/hsadminng/**/generated/**/*.class",
                    "net/hostsharing/hsadminng/hs/HsadminNgApplication.class"
            ])
        }))
    }
    doFirst { // Why not doLast? See README.md!
        println "HTML Jacoco Test Code Coverage Report: file://${reports.html.outputLocation.get()}/index.html"
    }
}
project.tasks.check.dependsOn(jacocoTestCoverageVerification)
jacocoTestCoverageVerification {
    violationRules {
        rule {
            limit {
                minimum = 0.80 // TODO.test: improve instruction coverage
            }
        }

        // element: PACKAGE, BUNDLE, CLASS, SOURCEFILE or METHOD
        // counter:  INSTRUCTION, BRANCH, LINE, COMPLEXITY, METHOD, or CLASS
        // value: TOTALCOUNT, COVEREDCOUNT, MISSEDCOUNT, COVEREDRATIO or MISSEDRATIO

        rule {
            element = 'CLASS'
            excludes = [
                    'net.hostsharing.hsadminng.**.generated.**',
                    'net.hostsharing.hsadminng.rbac.test.dom.TestDomainEntity',
                    'net.hostsharing.hsadminng.HsadminNgApplication',
                    'net.hostsharing.hsadminng.ping.PingController',
                    'net.hostsharing.hsadminng.rbac.generator.*',
                    'net.hostsharing.hsadminng.rbac.grant.RbacGrantsDiagramService',
                    'net.hostsharing.hsadminng.rbac.grant.RbacGrantsDiagramService.Node',
                    'net.hostsharing.hsadminng.**.*Repository',
                    'net.hostsharing.hsadminng.mapper.Mapper'
            ]

            limit {
                counter = 'LINE'
                value = 'COVEREDRATIO'
                minimum = 0.75 // TODO.test: improve line coverage
            }
        }
        rule {
            element = 'METHOD'
            excludes = [
                    'net.hostsharing.hsadminng.**.generated.**',
                    'net.hostsharing.hsadminng.HsadminNgApplication.main',
                    'net.hostsharing.hsadminng.ping.PingController.*'
            ]

            limit {
                counter = 'BRANCH'
                value = 'COVEREDRATIO'
                minimum = 0.00 // TODO.test: improve branch coverage
            }
        }
    }
}

// HOWTO: run all unit-tests which don't need a database: gw-test unitTest
tasks.register('unitTest', Test) {
    useJUnitPlatform {
        excludeTags 'importOfficeData', 'importHostingAssets', 'scenarioTest', 'generalIntegrationTest',
                'officeIntegrationTest', 'bookingIntegrationTest', 'hostingIntegrationTest'
    }

    group 'verification'
    description 'runs all unit-tests which do not need a database'

    mustRunAfter spotlessJava
}

// HOWTO: run all integration tests which are not specific to a module, like base, rbac, config etc.
tasks.register('generalIntegrationTest', Test) {
    useJUnitPlatform {
        includeTags 'generalIntegrationTest'
    }

    group 'verification'
    description 'runs integration tests which are not specific to a module, like base, rbac, config etc.'

    mustRunAfter spotlessJava
}

// HOWTO: run all integration tests of the office module: gw-test officeIntegrationTest
tasks.register('officeIntegrationTest', Test) {
    useJUnitPlatform {
        includeTags 'officeIntegrationTest'
    }

    group 'verification'
    description 'runs integration tests of the office module'

    mustRunAfter spotlessJava
}

// HOWTO: run all integration tests of the booking module: gw-test bookingIntegrationTest
tasks.register('bookingIntegrationTest', Test) {
    useJUnitPlatform {
        includeTags 'bookingIntegrationTest'
    }

    group 'verification'
    description 'runs integration tests of the booking module'

    mustRunAfter spotlessJava
}

// HOWTO: run all integration tests of the hosting module: gw-test hostingIntegrationTest
tasks.register('hostingIntegrationTest', Test) {
    useJUnitPlatform {
        includeTags 'hostingIntegrationTest'
    }

    group 'verification'
    description 'runs integration tests of the hosting module'

    mustRunAfter spotlessJava
}

tasks.register('importOfficeData', Test) {
    useJUnitPlatform {
        includeTags 'importOfficeData'
    }

    group 'verification'
    description 'run the import jobs as tests'

    mustRunAfter spotlessJava
}

tasks.register('importHostingAssets', Test) {
    useJUnitPlatform {
        includeTags 'importHostingAssets'
    }

    group 'verification'
    description 'run the import jobs as tests'

    mustRunAfter spotlessJava
}

tasks.register('scenarioTest', Test) {
    useJUnitPlatform {
        includeTags 'scenarioTest'
    }

    group 'verification'
    description 'run the import jobs as tests'

    mustRunAfter spotlessJava
}

// pitest mutation testing
pitest {
    targetClasses = ['net.hostsharing.hsadminng.**']
    excludedClasses = [
            'net.hostsharing.hsadminng.config.**',
            // 'net.hostsharing.hsadminng.**.*Controller',
            'net.hostsharing.hsadminng.**.generated.**'
    ]

    targetTests = ['net.hostsharing.hsadminng.**.*UnitTest', 'net.hostsharing.hsadminng.**.*RestTest']
    excludedTestClasses = ['**AcceptanceTest*', '**IntegrationTest*', '**ImportOfficeData', '**ImportHostingAssets']

    pitestVersion = '1.17.0'
    junit5PluginVersion = '1.1.0'

    threads = 4

    // As Java unit tests are pretty pointless in our case, this maybe makes not much sense.
    mutationThreshold = 71
    coverageThreshold = 57
    testStrengthThreshold = 87

    outputFormats = ['XML', 'HTML']
    timestampedReports = false
}
// project.tasks.check.dependsOn(project.tasks.pitest) TODO.test: PiTest currently does not work, needs to be fixed
project.tasks.pitest.doFirst { // Why not doLast? See README.md!
    println "PiTest Mutation Report: file:///${project.rootDir}/build/reports/pitest/index.html"
}


// Dependency Versions Upgrade
useLatestVersions {
    finalizedBy check
}

def isNonStable = { String version ->
    def stableKeyword = ['RELEASE', 'FINAL', 'GA'].any { it -> version.toUpperCase().contains(it) }
    def regex = /^[0-9,.v-]+(-r)?$/
    return !stableKeyword && !(version ==~ regex)
}

tasks.named("dependencyUpdates").configure {
    rejectVersionIf {
        isNonStable(it.candidate.version)
    }
}


// Generate HTML from Markdown scenario-test-reports using Pandoc:
tasks.register('convertMarkdownToHtml') {
    description = 'Generates HTML from Markdown scenario-test-reports using Pandoc.'
    group = 'Conversion'

    // Define the template file and input directory
    def templateFile = file('doc/scenarios/.template.html')

    // Task configuration and execution
    doFirst {
        // Check if pandoc is installed
        try {
            exec {
                commandLine 'pandoc', '--version'
            }
        } catch (Exception) {
            throw new GradleException("Pandoc is not installed or not found in the system path.")
        }

        // Check if the template file exists
        if (!templateFile.exists()) {
            throw new GradleException("Template file 'doc/scenarios/.template.html' not found.")
        }
    }

    doLast {
        // Gather all Markdown files in the current directory
        fileTree(dir: '.', include: 'build/doc/scenarios/*.md').each { file ->
            // Corrected way to create the output file path
            def outputFile = new File(file.parent, file.name.replaceAll(/\.md$/, '.html'))

            // Execute pandoc for each markdown file
            exec {
                commandLine 'pandoc', file.absolutePath, '--template', templateFile.absolutePath, '-o', outputFile.absolutePath
            }

            println "Converted ${file.name} to ${outputFile.name}"
        }
    }
}
convertMarkdownToHtml.dependsOn scenarioTest

// shortcut for compiling all files
tasks.register('compile') {
    dependsOn 'compileJava', 'compileTestJava'
}