/*
 * Copyright Thoughtworks, Inc.
 *
 * 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.
 */

import com.github.jk1.license.filter.LicenseBundleNormalizer
import com.github.jk1.license.render.JsonReportRenderer
import com.github.jk1.license.task.ReportTask
import com.thoughtworks.go.build.AdoptiumVersion
import com.thoughtworks.go.build.GoVersions
import com.thoughtworks.go.build.InstallerType
import de.undercouch.gradle.tasks.download.Download
import nl.javadude.gradle.plugins.license.License
import org.gradle.plugins.ide.idea.model.IdeaLanguageLevel

import java.security.SecureRandom
import java.util.stream.Collectors

plugins {
  id 'base'
  id 'org.owasp.dependencycheck' version '12.1.6'
  id 'com.autonomousapps.dependency-analysis' version '3.1.0'
  id 'com.github.hierynomus.license-base' version '0.16.1' apply false
  id 'com.github.jk1.dependency-license-report' version '2.9' apply false
}

// Updated via automated scripting
def GO_VERSION_PREVIOUS = '25.3.0'
def GO_VERSION_SEGMENTS = [year: 25, releaseInYear: 4, patch: 0]
def GO_VERSION_NEXT = '25.5.0'

def goVersions = new GoVersions(
  previousVersion: GO_VERSION_PREVIOUS,
  goVersion: [GO_VERSION_SEGMENTS.year, GO_VERSION_SEGMENTS.releaseInYear, GO_VERSION_SEGMENTS.patch].join('.'),
  nextVersion: GO_VERSION_NEXT,

  // Default target version. Can be overridden by sub-projects.
  targetJavaVersion: 17,

  // Version of JVM to compile and test using.
  buildJavaVersion: 21,

  // Version to package with installers and container images by default
  packagedJavaVersion: new AdoptiumVersion(
    feature: 21,
    interim: 0,    // set to `null` for the first release of a new featureVersion
    update: 8,    // set to `null` for the first release of a new featureVersion
    patch: null, // set to `null` for most releases. Patch releases are "exceptional".
    build: 9
  ),
)
ext.goVersions = goVersions
GoVersions.printGradleDebuggingOutputFor(gradle, goVersions)

def libraries = [
  // Dependabot will parse these dependencies.
  // Keep all of these as uninterpolated string literals so that Dependabot can parse the versions and create PRs for
  // upgrades.
  //
  // DO NOT interpolate version variables here because Dependabot is not smart enough to understand those. Dependabot's
  // version parsing is simply regex matching and never actually evaluates a gradle script.
  activeMQ            : 'org.apache.activemq:activemq-broker:6.1.7',
  angusMailSmtp       : 'org.eclipse.angus:smtp:2.0.5',
  apacheAnt           : 'org.apache.ant:ant:1.10.15',
  apacheHttpComponents: 'org.apache.httpcomponents:httpclient:4.5.14',
  aspectj             : 'org.aspectj:aspectjweaver:1.9.24',
  assertJ             : 'org.assertj:assertj-core:3.27.6',
  assertJ_DB          : 'org.assertj:assertj-db:3.0.0',
  awaitility          : 'org.awaitility:awaitility:4.3.0',
  bouncyCastle        : 'org.bouncycastle:bcprov-jdk18on:1.82',
  bucket4j            : 'com.bucket4j:bucket4j_jdk17-core:8.15.0',
  bucket4jCaffeine    : 'com.bucket4j:bucket4j-caffeine:8.10.1',
  caffeine            : 'com.github.ben-manes.caffeine:caffeine:3.2.2',
  cloning             : 'io.github.kostaskougios:cloning:1.12.0',
  commonsCodec        : 'commons-codec:commons-codec:1.19.0',
  commonsCollections  : 'commons-collections:commons-collections:3.2.2',
  commonsCollections4 : 'org.apache.commons:commons-collections4:4.5.0',
  commonsConfiguration: 'org.apache.commons:commons-configuration2:2.12.0',
  commonsDbcp         : 'org.apache.commons:commons-dbcp2:2.13.0',
  commonsFileUpload   : 'commons-fileupload:commons-fileupload:1.6.0',
  commonsIO           : 'commons-io:commons-io:2.20.0',
  commonsLang3        : 'org.apache.commons:commons-lang3:3.19.0',
  commonsPool         : 'org.apache.commons:commons-pool2:2.12.1',
  commonsText         : 'org.apache.commons:commons-text:1.14.0',
  dbunit              : 'org.dbunit:dbunit:3.0.0',
  dom4j               : 'org.dom4j:dom4j:2.2.0',
  ehcache             : 'net.sf.ehcache.internal:ehcache-core:2.11.1.3.11', // See https://repo.terracotta.org/maven2/net/sf/ehcache/internal/ehcache-core/maven-metadata.xml
  felix               : 'org.apache.felix:org.apache.felix.framework:7.0.5',
  freemarker          : 'org.freemarker:freemarker:2.3.34',
  gson                : 'com.google.code.gson:gson:2.13.2',
  h2                  : 'com.h2database:h2:1.4.200',
  hamcrest            : 'org.hamcrest:hamcrest:3.0',
  hibernate           : 'org.hibernate:hibernate-ehcache:3.6.10.Final',
  jacksonBom          : 'com.fasterxml.jackson:jackson-bom:2.20.0',
  jakartaAnnotation   : 'jakarta.annotation:jakarta.annotation-api:1.3.5',
  jakartaInject       : 'jakarta.inject:jakarta.inject-api:1.0.5',
  jakartaMail         : 'jakarta.mail:jakarta.mail-api:2.1.5',
  jakartaTransaction  : 'jakarta.transaction:jakarta.transaction-api:1.3.3',
  javassist           : 'org.javassist:javassist:3.30.2-GA',
  jaxbBom             : 'org.glassfish.jaxb:jaxb-bom:4.0.6',
  jaxen               : 'jaxen:jaxen:2.0.0',
  jcommander          : 'com.beust:jcommander:1.82',
  jdom                : 'org.jdom:jdom2:2.0.6.1',
  jetBrainsAnnotations: 'org.jetbrains:annotations:26.0.2-1',
  jetty               : 'org.eclipse.jetty:jetty-server:10.0.26',
  jgit                : 'org.eclipse.jgit:org.eclipse.jgit:6.10.1.202505221210-r',
  jolt                : 'com.bazaarvoice.jolt:jolt-core:0.1.8',
  jruby               : 'org.jruby:jruby-complete:9.4.14.0',
  jsonUnit            : 'net.javacrumbs.json-unit:json-unit-assertj:4.1.1',
  jsoup               : 'org.jsoup:jsoup:1.21.2',
  junit5Bom           : 'org.junit:junit-bom:6.0.0',
  liquibase           : 'org.liquibase:liquibase-core:5.0.1',
  liquibaseSlf4j      : 'com.mattbertolini:liquibase-slf4j:5.1.0',
  logback             : 'ch.qos.logback:logback-classic:1.5.19',
  lombok              : 'org.projectlombok:lombok:1.18.42',
  mockitoBom          : 'org.mockito:mockito-bom:5.20.0',
  mybatis             : 'org.mybatis:mybatis:3.5.19',
  mybatisSpring       : 'org.mybatis:mybatis-spring:2.1.2',
  mysql               : 'com.mysql:mysql-connector-j:9.4.0',
  objenesis           : 'org.objenesis:objenesis:3.4',
  oscache             : 'opensymphony:oscache:2.4.1',
  oshi                : 'com.github.oshi:oshi-core-java11:6.9.0',
  postgresql          : 'org.postgresql:postgresql:42.7.8',
  quartz              : 'org.quartz-scheduler:quartz:2.5.0',
  rack                : 'com.github.gocd-contrib:jruby-rack:1.3.0.7',
  resilience4jRetry   : 'io.github.resilience4j:resilience4j-retry:2.3.0',
  semanticVersion     : 'de.skuzzle:semantic-version:2.1.1',
  servletApi          : 'jakarta.servlet:jakarta.servlet-api:4.0.4',
  slf4jBom            : 'org.slf4j:slf4j-bom:2.0.17',
  spark               : 'com.sparkjava:spark-core:2.7.2',
  spring              : 'org.springframework:spring-core:4.3.30.RELEASE',
  springSecurity      : 'org.springframework.security:spring-security-config:4.2.20.RELEASE',
  springTestJunit5    : 'com.github.sbrannen:spring-test-junit5:1.5.0',
  systemStubs         : 'uk.org.webcompere:system-stubs-jupiter:2.1.8',
  testcontainersBom   : 'org.testcontainers:testcontainers-bom:1.21.3',
  tinybundles         : 'org.ops4j.pax.tinybundles:tinybundles:3.0.0',
  urlrewrite          : 'org.tuckey:urlrewritefilter:3.2.0',
  xmlUnit             : 'org.xmlunit:xmlunit-assertj:2.10.4',
  ztExec              : 'org.zeroturnaround:zt-exec:1.12',
]

// Export versions that are needed outside of this file (and elsewhere within)
def v = libraries.collectEntries { lib, libGav -> [lib, libGav.split(':').last()] } + [
  tanuki         : '3.6.2-st', // https://wrapper.tanukisoftware.com/doc/english/download.jsp#stable
  tanukiSha256sum: 'bef9540a93199c8bd7dab3b55607cbfb9576337dfac3aa8b11fd05ff96f81eb0',
  tfsSdk         : '14.139.0', // https://github.com/microsoft/team-explorer-everywhere/releases
  tfsSdkSha256sum: '744df70f70d28f6039938917ce02b07cbe07a577bdd662a64bb7dffb42512b90',
  tini           : '0.19.0', // https://github.com/krallin/tini/releases
] as Map<String, String>

// While Dependabot won't be able to parse these deps, these will get upgraded for free anyway since they share versions
// with dependencies declared above that are parseable by Dependabot, or are managed by platforms.
// This is just a workaround to be DRY while still benefiting from Dependabot's automatic PR upgrades.
def relatedLibraries = [
  apacheHttpMime          : "org.apache.httpcomponents:httpmime:${v.apacheHttpComponents}",
  bouncyCastlePkix        : "org.bouncycastle:bcpkix-jdk18on:${v.bouncyCastle}",
  jacksonCore             : "com.fasterxml.jackson.core:jackson-core",
  jacksonDatabind         : "com.fasterxml.jackson.core:jackson-databind",
  jaxbApi                 : "jakarta.xml.bind:jakarta.xml.bind-api",
  jaxbRuntime             : "org.glassfish.jaxb:jaxb-runtime",
  jettyDeploy             : "org.eclipse.jetty:jetty-deploy:${v.jetty}",
  jettyJmx                : "org.eclipse.jetty:jetty-jmx:${v.jetty}",
  jettyServlet            : "org.eclipse.jetty:jetty-servlet:${v.jetty}",
  jettyServlets           : "org.eclipse.jetty:jetty-servlets:${v.jetty}",
  jettyUtil               : "org.eclipse.jetty:jetty-util:${v.jetty}",
  jettyWebapp             : "org.eclipse.jetty:jetty-webapp:${v.jetty}",
  jettyWebsocket          : "org.eclipse.jetty.websocket:websocket-jetty-server:${v.jetty}",
  jgitServer              : "org.eclipse.jgit:org.eclipse.jgit.http.server:${v.jgit}",
  joltJsonUtils           : "com.bazaarvoice.jolt:json-utils:${v.jolt}",
  junit5Api               : "org.junit.jupiter:junit-jupiter-api",
  junit5Engine            : "org.junit.jupiter:junit-jupiter-engine",
  junit5Params            : "org.junit.jupiter:junit-jupiter-params",
  junit5PlatformLauncher  : "org.junit.platform:junit-platform-launcher",
  mockitoCore             : "org.mockito:mockito-core",
  mockitoJunit5           : "org.mockito:mockito-junit-jupiter",
  slf4jApi                : "org.slf4j:slf4j-api",
  slf4jJcl                : "org.slf4j:jcl-over-slf4j",
  slf4jJul                : "org.slf4j:jul-to-slf4j",
  springContext           : "org.springframework:spring-context:${v.spring}",
  springContextSupport    : "org.springframework:spring-context-support:${v.spring}",
  springOrm               : "org.springframework:spring-orm:${v.spring}",
  springSecurityWeb       : "org.springframework.security:spring-security-web:${v.springSecurity}",
  springTest              : "org.springframework:spring-test:${v.spring}",
  springTx                : "org.springframework:spring-tx:${v.spring}",
  springWeb               : "org.springframework:spring-web:${v.spring}",
  springWebmvc            : "org.springframework:spring-webmvc:${v.spring}",
  testcontainers          : "org.testcontainers:testcontainers",
  testcontainersJdbc      : "org.testcontainers:jdbc",
  testcontainersJunit     : "org.testcontainers:junit-jupiter",
  testcontainersMysql     : "org.testcontainers:mysql",
  testcontainersPostgresql: "org.testcontainers:postgresql",
]
//noinspection GroovyAssignabilityCheck
ext.deps = libraries + relatedLibraries
//noinspection GroovyAssignabilityCheck
ext.versions = v

allprojects {
  apply plugin: 'idea'
  apply plugin: 'com.autonomousapps.dependency-analysis'
  group = 'com.thoughtworks.go'
  version = goVersions.fullVersion
  layout.buildDirectory = "${projectDir}/target"

  repositories {
    mavenCentral()
    mavenLocal()
    maven { url = 'https://repo.terracotta.org/maven2/' } // Needed for later ehcache repos
    maven { url = 'https://jitpack.io' } // Needed for spring-test-junit5 and jruby-rack
  }

  if (project.path.startsWith(':api:') && !project.path.endsWith(':api-base')) {
    dependencyAnalysis {
      issues {
        onAny {
          // Ignore issues with dependencies on api-base, we rely on this to make API projects
          // easier to configure
          exclude(dependencies.project(path: ':api:api-base'))
        }
        onUsedTransitiveDependencies {
          // API projects use transitives from api-base rather than declaring everything directly
          severity('ignore')
        }
      }
    }
  }
}

idea {
  project {
    jdkName = goVersions.buildJavaVersion
    targetBytecodeVersion = JavaVersion.toVersion(goVersions.targetJavaVersion)
    languageLevel = new IdeaLanguageLevel(goVersions.targetJavaVersion)
  }
}

subprojects {
  if (!project.childProjects.isEmpty()) {
    return
  }
  apply plugin: 'base'
  apply plugin: 'com.github.hierynomus.license'
  license {
    skipExistingHeaders = false
    strictCheck = true
    exclude '**/.gitkeep'
    exclude '**/.tool-versions'
    exclude '**/*.enc'
    exclude '**/*.key'
    exclude '**/*.erb'
    exclude '**/*.ejs'
    exclude '**/*.keep'
    exclude '**/*.md'
    exclude '**/*.jar'
    exclude '**/*.json'
    exclude '**/*.html'
    exclude '**/*.zip'
    exclude '**/*.ico'
    exclude '**/*.png'
    exclude '**/*.jpg'
    exclude '**/*.jpeg'
    exclude '**/*.gif'
    exclude '**/*.woff'
    exclude '**/*.woff2'
    exclude '**/*.svg'
    exclude '**/*.json'
    exclude '**/*.pk1'
    exclude '**/*.pk8'
    exclude '**/*.crt'
    exclude '**/*.pass'
    exclude '**/*.p12'
    exclude '**/*.scss.d.ts'
    exclude '**/.*.json'
    exclude '**/*.lock'
    exclude '**/hg.template'
    exclude '**/available.toggles'
    exclude '**/META-INF/services/*'
    exclude '**/node_modules/**'
    exclude '**/node-vendor/**'
    exclude '**/vendor/**'
    exclude '**/javascripts/lib/**'
    exclude '**/gems/jruby/**'
    exclude '**/tmp/**'
    exclude '**/logs/**'
    exclude '**/log/**'
    exclude '**/robots.txt'
    exclude '**/rails/bin/**'
    exclude '**/rails/Gemfile*'
    exclude '**/rails/Rakefile*'
    exclude '**/rails/.babelrc'
    exclude '**/rails/.eslintignore'
    exclude '**/rails/.bundle/**'
    exclude '**/rails/.yarn/**'

    // Code borrowed from other projects
    exclude '**/JavaVersion.java'
    exclude '**/CommandLine.java'
    exclude '**/StreamPumper.java'
    exclude '**/StreamConsumer.java'
    exclude '**/RuntimeTypeAdapterFactory*.java'

    // used by intellij for type hints when editing ftl files, intellij gets confused if it sees a license header in this file
    exclude '**/freemarker_implicit.ftl'
    header = rootProject.file('build-platform/Apache-2.0.txt')
    headerDefinitions {
      intellijStyleXML {
        firstLine = "<!--"
        beforeEachLine = '  ~ '
        endLine = "  -->"
        afterEachLine = ''
        skipLinePattern = "^<\\?xml.*>\$"
        firstLineDetectionPattern = "(\\s|\\t)*<!--.*\$"
        lastLineDetectionPattern = ".*-->(\\s|\\t)*\$"
        allowBlankLines = true
        isMultiline = true
        padLines = false
      }

      intellijStyleFTL {
        firstLine = "<#--"
        beforeEachLine = ' '
        endLine = "  -->"
        afterEachLine = ''
        firstLineDetectionPattern = "(\\s|\\t)*<#--.*\$"
        lastLineDetectionPattern = ".*-->(\\s|\\t)*\$"
        allowBlankLines = true
        isMultiline = true
        padLines = false
      }
    }
    mapping {
      java = 'SLASHSTAR_STYLE'
      groovy = 'SLASHSTAR_STYLE'
      scss = 'SLASHSTAR_STYLE'
      sass = 'SLASHSTAR_STYLE'
      css = 'SLASHSTAR_STYLE'
      rb = 'SCRIPT_STYLE'
      rake = 'SCRIPT_STYLE'

      xml = 'intellijStyleXML'
      xsl = 'intellijStyleXML'
      xsd = 'intellijStyleXML'

      ftl = 'intellijStyleFTL'
      ftlh = 'intellijStyleFTL'
      ts = 'SLASHSTAR_STYLE'
      tsx = 'SLASHSTAR_STYLE'
      msx = 'SLASHSTAR_STYLE'
      js = 'SLASHSTAR_STYLE'
    }
  }
  check.dependsOn 'license'

  if (project.path.startsWithAny(":build-platform", ":installers", ":docker", ":release")) {
    return
  }

  final boolean hasJavaSources = !project.fileTree("src/main/java").files.isEmpty()
  if (project.path.startsWithAny(':server-launcher') || hasJavaSources) {
    apply plugin: 'com.github.jk1.dependency-license-report'

    licenseReport {
      renderers = [new JsonReportRenderer("index.json", false)]
      excludeGroups = ["com.thoughtworks.go"]
      filters = [new LicenseBundleNormalizer()]
      outputDir = layout.buildDirectory.dir("reports/dependency-license").get().asFile
      configurations = ['runtimeClasspath', 'packagingInLibDir']
    }

    tasks.withType(ReportTask).configureEach { ReportTask reportTask ->
      reportTask.outputs.cacheIf { true }
      configurations.each { config ->
        if (config.name in project.licenseReport.configurations) {
          reportTask.inputs.files(config)
        }
      }
    }
  }

  if (hasJavaSources) {
    apply plugin: 'java-library'

    layout.buildDirectory = "${projectDir}/target"

    configurations {
      packagingOnly
      extractedAtTopLevel { transitive = false }
      fatJarConfig
      testJavaAgents { transitive = false }
    }
    test.dependsOn configurations.testJavaAgents

    dependencies {
      // Force all sub-projects to have managed dependency versions for certain transitive dependencies
      api platform(project(':build-platform'))
      compileOnlyApi platform(project(':build-platform'))
      testJavaAgents("${project.deps.mockitoCore}:${project.versions.mockitoBom}")

      components {
        // workaround for Caffeine metadata declaring dependencies that are not needed at runtime
        withModule('com.github.ben-manes.caffeine:caffeine', { details ->
          details.allVariants {
            withDependencies { removeAll { it.name in ["jspecify", "error_prone_annotations"] } }
          }
        })
        withModule('com.google.code.gson:gson', { details ->
          details.allVariants {
            withDependencies { removeAll { it.name in ["error_prone_annotations"] } }
          }
        })
      }
    }
  }

  final boolean hasJavaTestFixtures = !project.fileTree("src/testFixtures").files.isEmpty()
  if (hasJavaTestFixtures) {
    apply plugin: 'java-test-fixtures'

    layout.buildDirectory = "${projectDir}/target"

    dependencies {
      // Force all sub-projects to have managed dependency versions for certain transitive dependencies
      testFixturesImplementation platform(project(':build-platform'))
    }
  }

  configurations.configureEach {
    resolutionStrategy {
      dependencySubstitution {
        substitute module("dom4j:dom4j") using module(project.deps.dom4j as String) because("Upgrade the legacy dom4j in old Hibernate 3.6; dom4j 2 should be sufficiently compatible")
        substitute module("javassist:javassist") using module(project.deps.javassist as String) because("Upgrade the legacy javassist in old Hibernate 3.6")
        substitute module("net.sf.ehcache:ehcache") using module(project.deps.ehcache as String) because("Use the much slimmer core artifact which is fine for our use case")
        substitute module("commons-logging:commons-logging") using module("${project.deps.slf4jJcl}:${project.versions.slf4jBom}") because("Replace commons-logging implementations with the slf4j apis that redirect logs to slf4j per https://www.slf4j.org/legacy.html")
        substitute module("com.bucket4j:bucket4j-core") using module(project.deps.bucket4j as String) because("Replace bucket4j-core with Java 17+ variant")

        substitute module("javax.annotation:javax.annotation-api") using module(project.deps.jakartaAnnotation as String) because("Javax Annotation has migrated to jakarta annotations")
        substitute module("javax.inject:javax.inject") using module(project.deps.jakartaInject as String) because("Javax Inject API has migrated to jakarta Inject API")
        substitute module("javax.servlet:javax.servlet-api") using module(project.deps.servletApi as String) because("Javax Servlet API has migrated to jakarta Servlet API")
        substitute module("javax.servlet:servlet-api") using module(project.deps.servletApi as String) because("Javax Servlet API has migrated to jakarta Servlet API")
        substitute module("org.eclipse.jetty.toolchain:jetty-servlet-api") using module(project.deps.servletApi as String) because("Javax Servlet API has migrated to jakarta Servlet API")
        substitute module("javax.transaction:jta") using module(project.deps.jakartaTransaction as String) because("Javax Transaction API has migrated to jakarta Transaction API")

        substitute module("org.hamcrest:hamcrest-core") using module(project.deps.hamcrest as String) because("Hamcrest core/library/all have been merged together: https://hamcrest.org/JavaHamcrest/distributables")
      }
    }
  }

  tasks.register('cleanLogs', Delete) {
    delete 'logs'
  }

  tasks.register('cleanApiOut', Delete) {
    delete 'out'
  }

  clean.dependsOn cleanLogs, cleanApiOut

  tasks.withType(Jar).configureEach { jarTask ->
    manifest {
      attributes(
        'Go-Version': project.version,
        'Go-Revision': goVersions.gitRevision,
        'Implementation-Title': project.name,
        'Implementation-Version': project.version
      )
    }

    includeEmptyDirs = false
    duplicatesStrategy = DuplicatesStrategy.EXCLUDE
  }

  tasks.withType(AbstractArchiveTask).configureEach {
    includeEmptyDirs = false
    duplicatesStrategy = DuplicatesStrategy.EXCLUDE

    preserveFileTimestamps = false
    reproducibleFileOrder = true
  }

  tasks.withType(Exec).configureEach {
    if (logger.isInfoEnabled()) {
      standardOutput = System.out
      errorOutput = System.err
    } else {
      standardOutput = new ByteArrayOutputStream()
      errorOutput = new ByteArrayOutputStream()
    }
  }

  tasks.withType(GroovyCompile).configureEach {
    options.incremental = true
  }

  tasks.withType(JavaCompile).configureEach {
    options.encoding = "UTF-8"
    options.compilerArgs << '-Werror'
    options.compilerArgs << '-Xlint:all,-serial,-deprecation,-processing,-this-escape,-classfile,-overloads'
    options.release.set(goVersions.targetJavaVersion as Integer)

    javaCompiler = javaToolchains.compilerFor {
      languageVersion = JavaLanguageVersion.of(goVersions.buildJavaVersion)
    }
  }

  tasks.withType(License).configureEach { License thisTask ->
    // Needs to be be done after evaluation for the task's source to be populated (either in afterEvaluate or doFirst)
    doFirst {
      def sourceSetName = thisTask.name.replaceFirst('licenseFormat', '').replaceFirst('license', '')
      def mappedSourceSet = sourceSets.find { sourceSet -> sourceSet.name.toLowerCase().equals(sourceSetName.toLowerCase()) } as SourceSet

      // Test resources are often just data files, and don't need license headers
      if (thisTask.name.toLowerCase().contains('test')) {
        thisTask.source -= mappedSourceSet.resources
      }
    }
  }

  tasks.withType(Test).configureEach { Test testTask ->
    dependsOn 'jar'

    testTask.useJUnitPlatform()
    javaLauncher = javaToolchains.launcherFor {
      languageVersion = JavaLanguageVersion.of(goVersions.buildJavaVersion)
    }

    def allTestClasses

    maxParallelForks = project.hasProperty('maxParallelForks') ? project.maxParallelForks as int : 1

    // fixup a tempdir that is predictable and we can clean it up later
    def tmpDir = project.file("${System.getProperty('java.io.tmpdir')}/gocd-tests/${new BigInteger(32, new SecureRandom()).toString(32)}")
    systemProperty 'java.io.tmpdir', tmpDir

    if (testTask.project.name == 'agent') {
      jvmArgs += InstallerType.agent.jvmInternalAccessArgs
    } else if (testTask.project.name.startsWithAny('server', 'config', 'common', 'domain', 'go-plugin-infra', 'api')) {
      // The tests for these projects often need modules to be opened or use server-style unsafe access which has warnings logged
      jvmArgs += InstallerType.server.jvmInternalAccessArgs
    }

    jvmArgs += testTask.project.configurations.hasProperty('testJavaAgents') ? ["-javaagent:${testTask.project.configurations.testJavaAgents.asPath}", "-XX:-PrintWarnings"] : []

    doFirst {
      classpath += rootProject.findProject(':test:test-utils').files("resource-include-in-all-projects")
    }

    defaultCharacterEncoding = "UTF-8"
    failOnNoDiscoveredTests = FilePartition.partitioningDisabled

    doFirst {
      logger.info("Setting tmpdir ${tmpDir}")
      delete(tmpDir)
      tmpDir.mkdirs()

      List<String> files = []
      testTask.testClassesDirs.files.each { File classDir ->
        if (classDir.exists()) {
          classDir.eachFileRecurse(groovy.io.FileType.FILES) { f ->
            def relPath = new File(classDir.toURI().relativize(f.toURI()).toString()).toString().replaceAll(/\$.*/, '.*').replaceAll(/\.class/, '.*')
            if (relPath.endsWith("Test.*")) files << relPath
          }
        }
      }
      files = files.unique()

      def partition = new FilePartition(files)
      def testsToRun = partition.currentFiles()
      allTestClasses = getTestClassesForTask(testsToRun)
      partition.logTo(rootProject.logger)

      (files - testsToRun).each { f ->
        testTask.exclude f
      }
      testsToRun.each { f ->
        testTask.include f
      }
    }

    beforeTest { TestDescriptor test ->
      allTestClasses.remove(test.className)
    }

    beforeSuite { TestDescriptor test ->
      allTestClasses.remove(test.className)
    }

    doLast {
      delete(tmpDir)

      if (!allTestClasses.empty && !filter.includePatterns.empty) {
        throw new GradleException("Missed the following classes to test: ${allTestClasses}".toString())
      }
    }

    testLogging {
      showStandardStreams = project.path != ':server'

      exceptionFormat = 'full'

      beforeSuite { suite ->
        if (suite.parent) {
          logger.quiet("Running ${suite.name}")
        }
      }

      afterSuite { suite, result ->
        if (suite.parent) {
          logger.quiet("Tests run: ${result.testCount}, Failures: ${result.failedTestCount}, Skipped: ${result.skippedTestCount}, Time elapsed: ${(result.endTime - result.startTime) / 1000.00} sec")
          if (result.resultType == TestResult.ResultType.FAILURE) {
            logger.quiet("Test ${suite.name} FAILED")
          }
        } else {
          logger.quiet("Total tests run: ${result.testCount}, Failures: ${result.failedTestCount}, Skipped: ${result.skippedTestCount}, Time elapsed: ${(result.endTime - result.startTime) / 1000.00} sec")
        }
      }
    }
  }

  tasks.withType(Delete).configureEach { Delete deleteTask ->
    idea {
      module {
        excludeDirs += deleteTask.targetFiles
      }
    }
  }
}

tasks.register('allDependencies') {
  dependsOn allprojects.collect { "$it.path:dependencies" }

  description = "Print dependency tree of all projects"
}

private allCompileTasks(Closure<Boolean> include = { t -> true }) {
  allprojects.collect { Project prj ->
    compileAll.dependsOn(prj.getTasks().withType(JavaCompile).findAll(include))
    compileAll.dependsOn(prj.getTasks().withType(GroovyCompile).findAll(include))
  }
}

tasks.register('compileAll') { compileAllTask ->
  group = LifecycleBasePlugin.BUILD_GROUP
  allCompileTasks()
}

tasks.register('prepare') {
  group = LifecycleBasePlugin.BUILD_GROUP
  allCompileTasks { Task task -> !task.name.toLowerCase().contains('test') && !task.project.path.toLowerCase().contains('test') }
}

tasks.register('sparkTest') { thisTask ->
  group = LifecycleBasePlugin.VERIFICATION_GROUP
  description = "Run all api tests"
  (project(':api').subprojects + project(':spark').subprojects).each { Project subPrj ->
    thisTask.dependsOn subPrj.tasks.withType(Test)
  }
  finalizedBy 'sparkTestJunitHtmlReport'
}

tasks.register('sparkTestJunitHtmlReport', TestReport) {
  dependsOn sparkTest

  testResults.from(project(':api').subprojects*.test.binaryResultsDirectory.locationOnly)
  testResults.from(project(':spark').subprojects*.test.binaryResultsDirectory.locationOnly)
  destinationDirectory = layout.buildDirectory.dir("reports/tests/sparkTest")
}

private void forAllTask(Closure<Task> closure) {
  subprojects.each { Project subPrj ->
    subPrj.tasks.withType(Test).each { eachTestTask ->
      if (!(eachTestTask.path in [':server:integrationTest'])) {
        closure.call(eachTestTask)
      }
    }
  }
}

tasks.register('allTests') { thisTask ->
  group = LifecycleBasePlugin.VERIFICATION_GROUP
  description = "Run all tests"

  forAllTask { Test eachTestTask ->
    thisTask.dependsOn eachTestTask
  }

  finalizedBy 'allTestsJunitHtmlReport'
}

tasks.register('allTestsJunitHtmlReport', TestReport) { TestReport thisTask ->
  group = LifecycleBasePlugin.VERIFICATION_GROUP
  description = "Run all api tests"

  forAllTask { Test eachTestTask ->
    testResults.from(eachTestTask.binaryResultsDirectory.locationOnly)
    eachTestTask.finalizedBy(thisTask)
  }
  destinationDirectory = layout.buildDirectory.dir("reports/tests/allTests")
}

static def toCamelCase(String input) {
  return input.split("[^a-zA-Z0-9]").collect { it.capitalize() }.join("")
}

static List<String> getTestClassesForTask(List<String> testsToRun) {
  return testsToRun.stream()
    .filter { testToRun -> !testToRun.matches(".*[/|\\\\].*Abstract.*Test.*") }
    .filter { testToRun -> testToRun.endsWith("Test.*") }
    .map { testToRun -> testToRun.replaceAll(".\\*", "").replaceAll("[^\\w.*]", ".") }
    .collect(Collectors.toList())
}

dependencyAnalysis {
  abi {
    exclusions {
      // Disable ABI analysis, as GoCD does not model module visibility correctly.
      excludeClasses(/com\.thoughtworks\.go\..*/)
    }
  }
  issues {
    all {
      onAny {
        exclude(project.deps.slf4jApi as String)
      }
      onCompileOnly {
        exclude(project.deps.jakartaAnnotation as String) // Spring needs at runtime
      }
      onIncorrectConfiguration {
      }
      onUsedTransitiveDependencies {
        // Exclude critical parts of frameworks which are annoying/pointless to declare individually
        excludeRegex(/.*:.*(core|commons)/)
        excludeRegex(/org.springframework:spring-(core|beans)/)
      }

    }

  }
}

dependencyCheck {
  failBuildOnCVSS = 1
  failBuildOnUnusedSuppressionRule = true
  suppressionFile = rootProject.file('build-platform/dependency-check-suppress.xml').toPath().toString()
  skipTestGroups = true

  nvd.apiKey = System.getenv("NVD_API_KEY") ?: ""
  analyzers.ossIndex.username = System.getenv("OSS_INDEX_USERNAME") ?: ""
  analyzers.ossIndex.password = System.getenv("OSS_INDEX_API_KEY") ?: ""

  analyzers.assemblyEnabled = false // Avoid dotnet false positives
  analyzers.retirejs.filterNonVulnerable = true // Reduces effort/memory processing retirejs results
}

dependencyCheckAggregate {
  def injected = project.objects.newInstance(Injected)
  doFirst {
    injected.execOps.exec {
      commandLine = [ "gem", "install", "bundler-audit", "--no-document" ]
      standardOutput = System.out
      errorOutput = System.err
    }
    injected.execOps.exec {
      commandLine = [ "bundle", "audit", "update" ]
      standardOutput = System.out
      errorOutput = System.err
    }
  }
}

dependencyCheck {
  data.directory = layout.buildDirectory.dir("dependency-check-data").get()
}

def downloadDependencyCheckCache = tasks.register('downloadDependencyCheckCache', Download) {
  src 'https://nexus.gocd.io/repository/cache/dependency-check-data.zip'
  dest layout.buildDirectory.file('dependency-check-data.zip')
  onlyIfModified true
  tempAndMove true
  validateStatus { it < 500 } // Continue on failure
}

tasks.register('unzipDependencyCheckCache', Copy) {
  dependsOn downloadDependencyCheckCache
  onlyIf { downloadDependencyCheckCache.map { it.dest.length() > 100 * 1024 } }
  from zipTree(downloadDependencyCheckCache.map { it.dest })
  into layout.buildDirectory.dir("dependency-check-data")
  def copyDetails = [] as List<FileCopyDetails>
  eachFile { FileCopyDetails fcd -> copyDetails << fcd }
  doLast {
    copyDetails.each { details ->
      def target = new File(destinationDir, details.path)
      if (target.exists()) {
        target.setLastModified(details.lastModified)
      }
    }
  }
}

dependencyCheckUpdate.dependsOn unzipDependencyCheckCache
dependencyCheckAnalyze.dependsOn unzipDependencyCheckCache
dependencyCheckAggregate.dependsOn unzipDependencyCheckCache

if (System.getenv().containsKey("NEXUS_CACHE_USERNAME")) {
  tasks.register('uploadDependencyCheckCache', Zip) {
    from layout.buildDirectory.file("dependency-check-data")
    archiveFileName = "dependency-check-data-updated.zip"
    destinationDirectory = layout.buildDirectory

    def injected = project.objects.newInstance(Injected)
    doLast {
      injected.execOps.exec {
        workingDir = projectDir
        commandLine = [
          "curl",
          "-u", "${System.getenv("NEXUS_CACHE_USERNAME")}:${System.getenv("NEXUS_CACHE_PASSWORD")}",
          "--include",
          "--upload-file", uploadDependencyCheckCache.archiveFile.get(), 'https://nexus.gocd.io/repository/cache/dependency-check-data.zip'
        ]
        standardOutput = System.out
        errorOutput = System.err
        ignoreExitValue = true
      }
    }
  }

  dependencyCheckUpdate.finalizedBy uploadDependencyCheckCache
  dependencyCheckAnalyze.finalizedBy uploadDependencyCheckCache
  dependencyCheckAggregate.finalizedBy uploadDependencyCheckCache
}

apply from: 'build-bootstrap-utils.gradle'
