import junitbuild.exec.CaptureJavaExecOutput
import junitbuild.exec.ClasspathSystemPropertyProvider
import junitbuild.exec.GenerateStandaloneConsoleLauncherShadowedArtifactsFile
import junitbuild.exec.RunConsoleLauncher
import junitbuild.extensions.isSnapshot
import junitbuild.extensions.javaModuleName
import junitbuild.javadoc.JavadocValuesOption
import junitbuild.javadoc.ModuleSpecificJavadocFileOption
import junitbuild.javadoc.VersionNumber
import org.gradle.api.tasks.PathSensitivity.RELATIVE
import java.nio.file.Files
import kotlin.io.path.writeLines

plugins {
	alias(libs.plugins.plantuml)
	id("junitbuild.antora-conventions")
	id("junitbuild.build-parameters")
	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_17
	testJavaVersion = JavaVersion.VERSION_17
}

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 allJavadocSinceValues = configurations.dependencyScope("allJavadocSinceValues")
val allJavadocSinceValuesClasspath = configurations.resolvable("allJavadocSinceValuesClasspath") {
	extendsFrom(allJavadocSinceValues.get())
	attributes {
		attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, named("javadoc-since-values"))
	}
}
val tools by sourceSets.creating
val toolsImplementation by configurations.getting

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

	compileOnlyApi(libs.jspecify)

	// 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)

	modularProjects.forEach { allJavadocSinceValues(it) }

	// 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.junitPlatformSuite)
	testImplementation(projects.junitPlatformTestkit)
	testImplementation(projects.junitVintageEngine)
	testImplementation(kotlin("stdlib"))
	testImplementation(libs.kotlinx.coroutines.test)
	testRuntimeOnly(kotlin("reflect"))

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

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

	standaloneConsoleLauncher(projects.junitPlatformConsoleStandalone)
}

val buildRevision: String by rootProject.extra
val snapshot = version.isSnapshot()
val releaseBranch = if (snapshot) "HEAD" else "r${version}"
val replaceCurrentDocs = buildParameters.documentation.replaceCurrentDocs
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/",
		"org.jspecify" to "https://jspecify.dev/docs/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=warning")
		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")
			systemProperty("junit.platform.discovery.issue.severity.critical", "warning")
		}
	}

	check {
		dependsOn(consoleLauncherTest)
	}

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

	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 plantUmlOutputDirectory = plantUml.flatMap { it.outputDirectory }

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

	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 mergeJavadocSinceValues by registering {
		inputs.files(allJavadocSinceValuesClasspath).withPathSensitivity(PathSensitivity.NONE)
		val outputFile = layout.buildDirectory.file("docs/aggregated-javadoc-since-values.txt")
		outputs.file(outputFile)
		outputs.cacheIf { true }
		doFirst {
			val initialVersions = setOf("1.0", "4.12", "5.0")
			val noPublicItems = setOf("1.3.1", "5.0.3", "5.4.1", "5.11.3")
			val excludes = initialVersions + noPublicItems
			val values = allJavadocSinceValuesClasspath.get().files.asSequence()
				.flatMap { it.readLines().asSequence() }
				.filter { it.isNotBlank() && it !in excludes }
				.sortedBy { VersionNumber(it) }
				.distinct()
			with(outputFile.get().asFile.toPath()) {
				Files.createDirectories(parent)
				writeLines(values)
			}
		}
	}

	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)
			addBooleanOption("-no-fonts", true)
			use(true)
			noTimestamp(true)

			addStringsOption("-module", ",").value = modularProjects.map { it.javaModuleName }
			addOption(ModuleSpecificJavadocFileOption("-module-source-path", modularProjects.associate { project ->
				project.javaModuleName to provider {
					files(
						project.sourceSets.named { it.startsWith("main") }.map {
							it.allJava.srcDirs.filter { it.exists() }
						}
					).asPath
				}
			}))
			addStringOption("-add-modules", "info.picocli,org.opentest4j.reporting.events,de.siegmar.fastcsv")
			addOption(ModuleSpecificJavadocFileOption("-add-reads", mapOf(
					"org.junit.platform.console" to provider { "info.picocli" },
					"org.junit.platform.reporting" to provider { "org.opentest4j.reporting.events" },
					"org.junit.jupiter.params" to provider { "de.siegmar.fastcsv" }
			)))

			val javadocSinceValuesFile: Provider<File> = mergeJavadocSinceValues.map { it.outputs.files.singleFile }
			inputs.file(javadocSinceValuesFile)
				.withPathSensitivity(PathSensitivity.NONE)
				.withPropertyName("javadoc-since-values-file")
			addOption(JavadocValuesOption("-since", javadocSinceValuesFile.map { file ->
				file.readLines().asSequence().filter { it.isNotBlank() }.toList()
			}))
		}

		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
	}

	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/junit-diamond.png">
						<link rel="icon" type="image/svg+xml" href="https://junit.org/assets/img/junit-diamond-adaptive.svg" sizes="any">
						""".trimIndent()

				val version = project.version.toString().replace("-SNAPSHOT", "")
				val targetUrl = if (buildParameters.ci)
					"https://docs.junit.org/$version"
				else
					project.antora.siteDir.get().asFile.toURI().resolve(version).toString()

				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)
					}

					result = result.replace("https://docs.junit.org/current", targetUrl)

					return@filter result
				}
			}
			filesMatching("**/stylesheet.css") {
				// Remove invalid import of `dejavu.css` due to `javadoc --no-fonts`
				filter { line -> if (line.startsWith("@import url('fonts/")) null else line }
			}
		}
		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 {
		asciidocAttributes.putAll(provider {
			mapOf(
				"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,
				"jdk-javadoc-base-url" to jdkJavadocBaseUrl
			)
		})
	}

	generateAntoraResources {
		dependsOn(generateAsciidocInputs, fixJavadoc)
	}
}
