package launchers

import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.attribute.PosixFilePermission
import java.util.jar.JarFile
import kotlin.io.path.createDirectories
import kotlin.io.path.createTempDirectory
import kotlin.io.path.writeText
import kotlin.test.Test
import kotlin.test.assertContains
import kotlin.test.assertContentEquals
import kotlin.test.assertEquals
import kotlin.test.assertNotEquals
import kotlin.test.assertNull

class LauncherIntegrationTest {
    private val projectDirectory = Path.of(System.getProperty("launcherProjectDirectory"))
    private val jarsDirectory = projectDirectory.resolve("build/libs")
    private val nativeLibrary = Path.of(System.getProperty("nativeExecLibrary"))
    private val nativeExecTestJar = Path.of(System.getProperty("nativeExecTestJar"))
    private val hostJava = Path.of(System.getProperty("java.home"), "bin", "java")

    @Test
    fun `wrapper jars contain only the runtime configuration they use`() {
        val downloadJars =
            listOf(
                "minecraft-java-server-wrapper.jar",
                "minecraft-fabric-server-wrapper.jar",
                "geyser-standalone-wrapper.jar",
                "mcxboxbroadcast-standalone-wrapper.jar",
            )
        val supervisorJars = listOf("minecraft-server-java.jar", "minecraft-server-fabric.jar")
        val expectedVersions = Files.readAllBytes(projectDirectory.resolve("../upstream-versions.json"))

        for (jarName in downloadJars) {
            JarFile(jarsDirectory.resolve(jarName).toFile()).use { jar ->
                assertContentEquals(expectedVersions, jar.readEntry("upstream-versions.json"), jarName)
                assertNull(jar.getJarEntry("launchers/native-library-path"), jarName)
            }
        }
        for (jarName in supervisorJars) {
            JarFile(jarsDirectory.resolve(jarName).toFile()).use { jar ->
                assertNull(jar.getJarEntry("upstream-versions.json"), jarName)
                assertNull(jar.getJarEntry("launchers/native-library-path"), jarName)
            }
        }
    }

    @Test
    fun `launcher command builders preserve JVM and application arguments`() {
        assertEquals(Path.of(System.getProperty("java.home"), "bin", "java").toString(), javaExecutable())

        assertEquals(
            listOf(
                "/jdk/bin/java",
                "-Dwrapper.test=preserved",
                "-Xms32m",
                "-jar",
                "/data/server.jar",
                "--cli-argument",
                "with spaces",
            ),
            applicationCommand(
                "/jdk/bin/java",
                listOf("-Dwrapper.test=preserved", "-Xms32m"),
                Path.of("/data/server.jar"),
                arrayOf("--cli-argument", "with spaces"),
            ),
        )

        assertEquals(
            listOf(
                "/usr/libexec/minecraft-server-java-supervisor",
                "minecraft-server-supervisor-v1",
                "/jdk/bin/java",
                "2",
                "-Dwrapper.test=preserved",
                "-Xms32m",
                "--cli-argument",
                "with spaces",
            ),
            supervisorCommand(
                "/usr/libexec/minecraft-server-java-supervisor",
                "/jdk/bin/java",
                listOf("-Dwrapper.test=preserved", "-Xms32m"),
                arrayOf("--cli-argument", "with spaces"),
            ),
        )
    }

    @Test
    fun `cached jars must match their locked checksum`() {
        val root = createTempDirectory("jar-checksum-test-")
        try {
            val jar = root.resolve("server.jar")
            jar.writeText("valid")
            assertEquals(
                true,
                hasExpectedSha256(jar, "ec654fac9599f62e79e2706abef23dfb7c07c08185aa86db4d8695f0b718d1b3"),
            )
            jar.writeText("corrupt")
            assertEquals(
                false,
                hasExpectedSha256(jar, "ec654fac9599f62e79e2706abef23dfb7c07c08185aa86db4d8695f0b718d1b3"),
            )
        } finally {
            root.toFile().deleteRecursively()
        }
    }

    @Test
    fun `native bridge replaces the wrapper process and preserves arguments`() {
        Fixture().use { fixture ->
            assertEquals(
                listOf("--cli-argument", "with spaces"),
                fixture.runNativeExec("--cli-argument", "with spaces"),
            )
        }
    }

    @Test
    fun `native bridge reports exec failures as Java exceptions`() {
        val process =
            ProcessBuilder(
                hostJava.toString(),
                "-Djava.library.path=${nativeLibrary.parent}",
                "-jar",
                nativeExecTestJar.toString(),
                "/path/that/does/not/exist",
            ).apply {
                redirectErrorStream(true)
            }.start()
        val output = process.inputStream.bufferedReader().readText()

        assertNotEquals(0, process.waitFor(), output)
        assertContains(output, "java.io.IOException")
        assertContains(output, "No such file or directory")
    }

    private inner class Fixture : AutoCloseable {
        private val root = createTempDirectory("jar-launcher-test-")
        private val executable = executable(root.resolve("executable"))

        fun runNativeExec(vararg arguments: String): List<String> {
            val process =
                ProcessBuilder(
                    hostJava.toString(),
                    "-Djava.library.path=${nativeLibrary.parent}",
                    "-jar",
                    nativeExecTestJar.toString(),
                    executable.toString(),
                    *arguments,
                ).apply {
                    redirectErrorStream(true)
                }.start()
            val output = process.inputStream.bufferedReader().readLines()
            assertEquals(0, process.waitFor(), output.joinToString("\n"))
            return output
        }

        override fun close() {
            root.toFile().deleteRecursively()
        }

        private fun executable(path: Path): Path {
            path.parent.createDirectories()
            path.writeText(
                """
                #!/usr/bin/bash
                printf '%s\n' "${'$'}@"
                """.trimIndent() + "\n",
            )
            Files.setPosixFilePermissions(
                path,
                setOf(
                    PosixFilePermission.OWNER_READ,
                    PosixFilePermission.OWNER_WRITE,
                    PosixFilePermission.OWNER_EXECUTE,
                    PosixFilePermission.GROUP_READ,
                    PosixFilePermission.GROUP_EXECUTE,
                    PosixFilePermission.OTHERS_READ,
                    PosixFilePermission.OTHERS_EXECUTE,
                ),
            )
            return path
        }
    }

    private fun JarFile.readEntry(name: String): ByteArray? =
        getJarEntry(name)?.let { entry -> getInputStream(entry).use { it.readBytes() } }
}
