import junitbuild.exec.CaptureJavaExecOutput
import junitbuild.exec.ClasspathSystemPropertyProvider
import junitbuild.exec.GenerateStandaloneConsoleLauncherShadowedArtifactsFile
import junitbuild.exec.RunConsoleLauncher
import junitbuild.extensions.dependencyProject
import junitbuild.extensions.isSnapshot
import junitbuild.extensions.javaModuleName
import junitbuild.javadoc.ModuleSpecificJavadocFileOption
import org.gradle.api.tasks.PathSensitivity.RELATIVE

plugins {
	alias(libs.plugins.plantuml)
	id("junitbuild.antora-conventions")
	id("junitbuild.build-parameters")
	id("junitbuild.java-multi-release-test-sources")
	id("junitbuild.kotlin-library-conventions")
	id("junitbuild.testing-conventions")
}

val mavenizedProjects: List<Project> by rootProject
val modularProjects: List<Project> by rootProject

// Because we need to set up Javadoc aggregation
modularProjects.forEach { evaluationDependsOn(it.path) }

javaLibrary {
	mainJavaVersion = JavaVersion.VERSION_1_8
	testJavaVersion = JavaVersion.VERSION_1_8
}

val apiReport = configurations.dependencyScope("apiReport")
val apiReportClasspath = configurations.resolvable("apiReportClasspath") {
	extendsFrom(apiReport.get())
}
val standaloneConsoleLauncher = configurations.dependencyScope("standaloneConsoleLauncher")
val standaloneConsoleLauncherClasspath = configurations.resolvable("standaloneConsoleLauncherClasspath") {
	extendsFrom(standaloneConsoleLauncher.get())
}
val attestation = configurations.dependencyScope("attestation")
val attestationClasspath = configurations.resolvable("attestationClasspath") {
	extendsFrom(attestation.get())
	isTransitive = false
}

val tools by sourceSets.creating
val toolsImplementation by configurations.getting

dependencies {
	implementation(projects.junitJupiterApi) {
		because("Jupiter API is used in src/main/java")
	}

	// Pull in all "modular projects" to ensure that they are included
	// in reports generated by the ApiReportGenerator.
	modularProjects.forEach { apiReport(it) }
	apiReport(libs.openTestReporting.tooling.spi)

	// Pull in all "mavenized projects" to ensure that they are included
	// in the generation of build provenance attestation.
	mavenizedProjects.forEach { attestation(it) }

	testImplementation(projects.junitJupiterMigrationsupport)
	testImplementation(projects.junitPlatformConsole)
	testImplementation(projects.junitPlatformRunner)
	testImplementation(projects.junitPlatformSuite)
	testImplementation(projects.junitPlatformTestkit)
	testImplementation(projects.junitVintageEngine)
	testImplementation(kotlin("stdlib"))

	toolsImplementation(projects.junitPlatformCommons)
	toolsImplementation(libs.classgraph)
	toolsImplementation(libs.apiguardian)

	testImplementation(libs.jimfs) {
		because("Jimfs is used in src/test/java")
	}

	if (java.toolchain.implementation.orNull == JvmImplementation.J9) {
		testRuntimeOnly(libs.jfrPolyfill) {
			because("OpenJ9 does not include JFR")
		}
	}

	standaloneConsoleLauncher(projects.junitPlatformConsoleStandalone)
}

val buildRevision: String by rootProject.extra
val snapshot = version.isSnapshot()
val docsVersion = if (snapshot) "snapshot" else version
val releaseBranch = if (snapshot) "HEAD" else "r${version}"
val docsDir = layout.buildDirectory.dir("ghpages-docs")
val replaceCurrentDocs = buildParameters.documentation.replaceCurrentDocs
val uploadPdfs = !snapshot
val userGuidePdfFileName = "junit-user-guide-${version}.pdf"
val ota4jDocVersion = libs.versions.opentest4j.map { if (it.isSnapshot()) "snapshot" else it }.get()
val apiGuardianDocVersion = libs.versions.apiguardian.map { if (it.isSnapshot()) "snapshot" else it }.get()

val generatedAsciiDocPath = layout.buildDirectory.dir("generated/asciidoc")
val consoleLauncherOptionsFile = generatedAsciiDocPath.map { it.file("console-launcher-options.txt") }
val consoleLauncherDiscoverOptionsFile = generatedAsciiDocPath.map { it.file("console-launcher-discover-options.txt") }
val consoleLauncherExecuteOptionsFile = generatedAsciiDocPath.map { it.file("console-launcher-execute-options.txt") }
val consoleLauncherEnginesOptionsFile = generatedAsciiDocPath.map { it.file("console-launcher-engines-options.txt") }
val experimentalApisTableFile = generatedAsciiDocPath.map { it.file("experimental-apis-table.adoc") }
val deprecatedApisTableFile = generatedAsciiDocPath.map { it.file("deprecated-apis-table.adoc") }
val standaloneConsoleLauncherShadowedArtifactsFile = generatedAsciiDocPath.map { it.file("console-launcher-standalone-shadowed-artifacts.adoc") }

val jdkJavadocBaseUrl = "https://docs.oracle.com/en/java/javase/${JavaVersion.current().majorVersion}/docs/api"
val elementListsDir = layout.buildDirectory.dir("elementLists")
val externalModulesWithoutModularJavadoc = mapOf(
		"org.apiguardian.api" to "https://apiguardian-team.github.io/apiguardian/docs/$apiGuardianDocVersion/api/",
		"org.assertj.core" to "https://javadoc.io/doc/org.assertj/assertj-core/${libs.versions.assertj.get()}/",
		"org.opentest4j" to "https://ota4j-team.github.io/opentest4j/docs/$ota4jDocVersion/api/"
)
require(externalModulesWithoutModularJavadoc.values.all { it.endsWith("/") }) {
	"all base URLs must end with a trailing slash: $externalModulesWithoutModularJavadoc"
}

tasks {

	val consoleLauncherTestReportsDir = project.layout.buildDirectory.dir("console-launcher-test-results")
	val consoleLauncherTestEventXmlFiles =
		files(consoleLauncherTestReportsDir.map { it.asFileTree.matching { include("**/open-test-report.xml") } })

	val consoleLauncherTest by registering(RunConsoleLauncher::class) {
		args.addAll("execute")
		args.addAll("--scan-classpath")
		args.addAll("--config=junit.platform.reporting.open.xml.enabled=true")
		args.addAll("--config=junit.platform.reporting.open.xml.git.enabled=true")
		args.addAll("--config=junit.platform.output.capture.stdout=true")
		args.addAll("--config=junit.platform.output.capture.stderr=true")
		args.addAll("--config=junit.platform.discovery.issue.severity.critical=info")
		outputs.dir(consoleLauncherTestReportsDir)
		argumentProviders.add(CommandLineArgumentProvider {
			listOf(
				"--reports-dir=${consoleLauncherTestReportsDir.get()}",
			)
		})
		args.addAll("--include-classname", ".*Tests")
		args.addAll("--include-classname", ".*Demo")
		args.addAll("--exclude-tag", "exclude")
		args.addAll("--exclude-tag", "timeout")

		doFirst {
			consoleLauncherTestReportsDir.get().asFile.deleteRecursively()
		}

		finalizedBy(generateOpenTestHtmlReport)
	}

	generateOpenTestHtmlReport {
		mustRunAfter(consoleLauncherTest)
		inputs.files(consoleLauncherTestEventXmlFiles).withPathSensitivity(RELATIVE).skipWhenEmpty()
		argumentProviders += CommandLineArgumentProvider {
			consoleLauncherTestEventXmlFiles.files.map { it.absolutePath }.toList()
		}
	}

	register<RunConsoleLauncher>("consoleLauncher") {
		hideOutput = false
		outputs.upToDateWhen { false }
	}

	test {
		include("**/*Demo.class")
		(options as JUnitPlatformOptions).apply {
			includeEngines("junit-vintage")
			includeTags("timeout")
		}
	}

	testRelease21 {
		include("**/*Demo.class")
	}

	check {
		dependsOn(consoleLauncherTest)
	}

	named<JavaCompile>(tools.compileJavaTaskName) {
		options.release.set(21)
	}

	named<Checkstyle>("checkstyleTools") {
		config = resources.text.fromFile(checkstyle.configDirectory.file("checkstyleMain.xml"))
	}

	val generateConsoleLauncherOptions by registering(CaptureJavaExecOutput::class) {
		classpath.from(standaloneConsoleLauncherClasspath)
		mainClass = "org.junit.platform.console.ConsoleLauncher"
		args.addAll("--help", "--disable-banner")
		outputFile = consoleLauncherOptionsFile
	}

	val generateConsoleLauncherDiscoverOptions by registering(CaptureJavaExecOutput::class) {
		classpath.from(standaloneConsoleLauncherClasspath)
		mainClass = "org.junit.platform.console.ConsoleLauncher"
		args.addAll("discover", "--help", "--disable-banner")
		outputFile = consoleLauncherDiscoverOptionsFile
	}

	val generateConsoleLauncherExecuteOptions by registering(CaptureJavaExecOutput::class) {
		classpath.from(standaloneConsoleLauncherClasspath)
		mainClass = "org.junit.platform.console.ConsoleLauncher"
		args.addAll("execute", "--help", "--disable-banner")
		outputFile = consoleLauncherExecuteOptionsFile
	}

	val generateConsoleLauncherEnginesOptions by registering(CaptureJavaExecOutput::class) {
		classpath.from(standaloneConsoleLauncherClasspath)
		mainClass = "org.junit.platform.console.ConsoleLauncher"
		args.addAll("engines", "--help", "--disable-banner")
		outputFile = consoleLauncherEnginesOptionsFile
	}

	val generateApiTables by registering(JavaExec::class) {
		classpath = tools.runtimeClasspath
		mainClass = "org.junit.api.tools.ApiReportGenerator"
		systemProperty("api.moduleNames", modularProjects.map { it.javaModuleName }.sorted().joinToString(","))
		jvmArgumentProviders += ClasspathSystemPropertyProvider("api.modulePath", apiReportClasspath.get())
		argumentProviders += CommandLineArgumentProvider {
			listOf(
				"DEPRECATED=${deprecatedApisTableFile.get().asFile.absolutePath}",
				"EXPERIMENTAL=${experimentalApisTableFile.get().asFile.absolutePath}",
			)
		}
		outputs.cacheIf { true }
		outputs.file(deprecatedApisTableFile)
		outputs.file(experimentalApisTableFile)
	}

	val generateStandaloneConsoleLauncherShadowedArtifactsFile by registering(GenerateStandaloneConsoleLauncherShadowedArtifactsFile::class) {
		inputJar.fileProvider(standaloneConsoleLauncherClasspath.flatMap { it.elements.map { it.single().asFile } })
		outputFile = standaloneConsoleLauncherShadowedArtifactsFile
	}

	plantUml {
		fileFormat = "SVG"
		outputs.cacheIf { true }
	}

	val componentDiagram = plantUml.flatMap { it.outputDirectory.file("component-diagram.svg") }

	val generateAsciidocInputs by registering {
		dependsOn(
			generateConsoleLauncherOptions,
			generateConsoleLauncherDiscoverOptions,
			generateConsoleLauncherExecuteOptions,
			generateConsoleLauncherEnginesOptions,
			generateApiTables,
			generateStandaloneConsoleLauncherShadowedArtifactsFile,
			componentDiagram
		)
	}

	val downloadJavadocElementLists by registering {
		outputs.cacheIf { true }
		outputs.dir(elementListsDir).withPropertyName("elementListsDir")
		inputs.property("externalModulesWithoutModularJavadoc", externalModulesWithoutModularJavadoc)
		doFirst {
			externalModulesWithoutModularJavadoc.forEach { (moduleName, baseUrl) ->
				val resource = resources.text.fromUri("${baseUrl}element-list")
				elementListsDir.get().asFile.resolve(moduleName).apply {
					mkdir()
					resolve("element-list").writeText("module:$moduleName\n${resource.asString()}")
				}
			}
		}
	}

	val aggregateJavadocs by registering(Javadoc::class) {
		dependsOn(modularProjects.map { it.tasks.jar })
		dependsOn(downloadJavadocElementLists)
		group = "Documentation"
		description = "Generates aggregated Javadocs"

		title = "JUnit $version API"

		val additionalStylesheetFile = "src/javadoc/junit-stylesheet.css"
		inputs.file(additionalStylesheetFile)
		val overviewFile = "src/javadoc/junit-overview.html"
		inputs.file(overviewFile)

		options {

			memberLevel = JavadocMemberLevel.PROTECTED
			header = rootProject.description
			encoding = "UTF-8"
			locale = "en"
			overview = overviewFile
			jFlags("-Xmx1g")

			this as StandardJavadocDocletOptions
			splitIndex(true)
			addBooleanOption("Xdoclint:all,-missing", true)
			addBooleanOption("Werror", true)
			addBooleanOption("html5", true)
			addMultilineStringsOption("tag").value = listOf(
					"apiNote:a:API Note:",
					"implNote:a:Implementation Note:"
			)

			links(jdkJavadocBaseUrl)
			links("https://junit.org/junit4/javadoc/${libs.versions.junit4.get()}/")
			externalModulesWithoutModularJavadoc.forEach { (moduleName, baseUrl) ->
				linksOffline(baseUrl, elementListsDir.get().asFile.resolve(moduleName).absolutePath)
			}

			groups = mapOf(
					"Jupiter" to listOf("org.junit.jupiter*"),
					"Vintage" to listOf("org.junit.vintage*"),
					"Platform" to listOf("org.junit.platform*")
			)
			addStringOption("-add-stylesheet", additionalStylesheetFile)
			use(true)
			noTimestamp(true)

			addStringsOption("-module", ",").value = modularProjects.map { it.javaModuleName }
			val moduleSourcePathOption = addPathOption("-module-source-path")
			moduleSourcePathOption.value = modularProjects.map { it.file("src/module") }
			moduleSourcePathOption.value.forEach { inputs.dir(it) }
			addOption(ModuleSpecificJavadocFileOption("-patch-module", modularProjects.associate { project ->
				project.javaModuleName to files(
					project.sourceSets.named { it.startsWith("main") }.map { it.allJava.srcDirs }
				).asPath
			}))
			addStringOption("-add-modules", "info.picocli,org.opentest4j.reporting.events")
			addOption(ModuleSpecificJavadocFileOption("-add-reads", mapOf(
					"org.junit.platform.console" to "info.picocli",
					"org.junit.platform.reporting" to "org.opentest4j.reporting.events",
					"org.junit.jupiter.params" to "univocity.parsers"
			)))
		}

		source(modularProjects.map { project ->
			files(project.sourceSets.named { it.startsWith("main") }.map { it.allJava })
		})
		classpath = files(modularProjects.map { it.sourceSets.main.get().compileClasspath })

		setMaxMemory("1024m")
		options.destinationDirectory = layout.buildDirectory.dir("docs/javadoc").get().asFile

		doFirst {
			(options as CoreJavadocOptions).modulePath = classpath.files.toList()
		}
	}

	val fixJavadoc by registering(Copy::class) {
		dependsOn(aggregateJavadocs)
		group = "Documentation"
		description = "Fix links to external API specs in the locally aggregated Javadoc HTML files"

		val inputDir = aggregateJavadocs.map { it.destinationDir!! }
		inputs.property("externalModulesWithoutModularJavadoc", externalModulesWithoutModularJavadoc)
		from(inputDir.map { File(it, "element-list") }) {
			// For compatibility with pre JDK 10 versions of the Javadoc tool
			rename { "package-list" }
		}
		from(inputDir) {
			filesMatching("**/*.html") {
				val favicon = "<link rel=\"icon\" type=\"image/png\" href=\"https://junit.org/assets/img/junit5-logo.png\">"
				filter { line ->
					var result = if (line.startsWith("<head>")) line.replace("<head>", "<head>$favicon") else line
					externalModulesWithoutModularJavadoc.forEach { (moduleName, baseUrl) ->
						result = result.replace("${baseUrl}$moduleName/", baseUrl)
					}
					return@filter result
				}
			}
		}
		into(layout.buildDirectory.dir("docs/fixedJavadoc"))
	}

	val prepareGitHubAttestation by registering(Sync::class) {
		from(attestationClasspath)
		into(layout.buildDirectory.dir("attestation"))
		rename("(.*)-SNAPSHOT.jar", "$1-SNAPSHOT+${buildRevision.substring(0, 7)}.jar")
	}

	generateAntoraYml {
		val platformVersion: String by project
		val vintageVersion: String by project
		asciidocAttributes.putAll(provider {
			mapOf(
				"jupiter-version" to project.version,
				"platform-version" to platformVersion,
				"vintage-version" to vintageVersion,
				"bom-version" to project.version,
				"junit4-version" to libs.versions.junit4.get(),
				"apiguardian-version" to libs.versions.apiguardian.get(),
				"ota4j-version" to libs.versions.opentest4j.get(),
				"surefire-version" to libs.versions.surefire.get(),
				"release-branch" to releaseBranch,
				"docs-version" to docsVersion,
				"jdk-javadoc-base-url" to jdkJavadocBaseUrl
			)
		})
	}

	generateAntoraResources {
		dependsOn(generateAsciidocInputs, fixJavadoc)
	}
}

eclipse {
	classpath {
		plusConfigurations.add(dependencyProject(projects.junitPlatformConsole).configurations["shadowedClasspath"])
		plusConfigurations.add(dependencyProject(projects.junitJupiterParams).configurations["shadowedClasspath"])
	}
}

idea {
	module {
		scopes["PROVIDED"]!!["plus"]!!.add(dependencyProject(projects.junitPlatformConsole).configurations["shadowedClasspath"])
		scopes["PROVIDED"]!!["plus"]!!.add(dependencyProject(projects.junitJupiterParams).configurations["shadowedClasspath"])
	}
}
