package dev.rnett.gradle.mcp.gradle

import dev.rnett.gradle.mcp.fixtures.gradle.testGradleProject
import dev.rnett.gradle.mcp.fixtures.gradle.withTestGradleDefaults
import dev.rnett.gradle.mcp.gradle.build.BuildComponentOutcome
import dev.rnett.gradle.mcp.gradle.build.BuildOutcome
import dev.rnett.gradle.mcp.utils.OS
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import kotlin.time.Duration.Companion.minutes
import kotlin.time.Duration.Companion.seconds

@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class TaskOutInitScriptTest {

    private lateinit var buildManager: BuildManager
    private lateinit var provider: DefaultGradleProvider

    @BeforeAll
    fun setupAll() {
        buildManager = BuildManager()
        provider = DefaultGradleProvider(
            buildManager = buildManager
        )
    }

    @AfterAll
    fun cleanupAll() {
        provider.close()
        buildManager.close()
    }


    @Test
    fun `task-out init script is loaded`() = runTest(timeout = 300.seconds) {
        testGradleProject {
            buildScript("")
        }.use { project ->
            val projectRoot = GradleProjectRoot(project.pathString())
            val args = GradleInvocationArguments(
                additionalArguments = listOf("help", "-Pgradle-mcp.init-scripts.hello"),
                envSource = EnvSource.NONE
            ).withTestGradleDefaults().withInitScript("task-out")

            val runningBuild = provider.runBuild(
                projectRoot = projectRoot,
                args = args
            )
            val result = runningBuild.awaitFinished()

            assert(result.outcome is BuildOutcome.Success)
            assert(result.consoleOutput.contains("Gradle MCP init script task-out.init.gradle.kts loaded"))
        }
    }

    @Test
    fun `task-out init script prefixes task output`() = runTest(timeout = 120.seconds) {
        testGradleProject {
            buildScript(
                """
                tasks.register("printMessage") {
                    doLast {
                        println("Hello from task")
                        System.err.println("Error from task")
                    }
                }
            """.trimIndent()
            )
        }.use { project ->
            val projectRoot = GradleProjectRoot(project.pathString())
            val args = GradleInvocationArguments(
                additionalArguments = listOf("printMessage")
            ).withTestGradleDefaults().withInitScript("task-out")

            val runningBuild = provider.runBuild(
                projectRoot = projectRoot,
                args = args
            )
            val result = runningBuild.awaitFinished()

            assert(result.outcome is BuildOutcome.Success)
            assert(!result.taskOutputCapturingFailed)

            assert(result.consoleOutput.contains(":printMessage OUT Hello from task"))
            assert(result.consoleOutput.contains(":printMessage ERR Error from task"))

            // Raw stdout lines should be suppressed (held pending and discarded when the
            // structured [gradle-mcp] [task-output] version arrives on the same stream).
            // Raw stderr lines may still appear because the structured replacement arrives
            // on stdout, not stderr, so the stderr stream can't suppress them.
            val lines = result.consoleOutput.lines()
            assert(!lines.contains("Hello from task"))

            assert(result.getTaskOutput(":printMessage") == "Hello from task\nError from task")

            val taskResult = result.taskResults[":printMessage"]
            assert(taskResult != null)
            assert(taskResult!!.outcome == BuildComponentOutcome.SUCCESS)
            assert(taskResult.consoleOutput?.trim() == "Hello from task\nError from task")
            assert(taskResult.duration >= 0.seconds)
        }
    }

    @Test
    fun `task-out init script prefixes external process output`() = runTest(timeout = 120.seconds) {
        val command = if (OS.isWindows) {
            "listOf(\"cmd\", \"/c\", \"echo Hello from external process\")"
        } else {
            "listOf(\"sh\", \"-c\", \"echo Hello from external process\")"
        }

        testGradleProject {
            buildScript(
                """
                tasks.register<Exec>("execTask") {
                    commandLine($command)
                }
            """.trimIndent()
            )
        }.use { project ->
            val projectRoot = GradleProjectRoot(project.pathString())
            val args = GradleInvocationArguments(
                additionalArguments = listOf("execTask")
            ).withTestGradleDefaults().withInitScript("task-out")

            val runningBuild = provider.runBuild(
                projectRoot = projectRoot,
                args = args
            )
            val result = runningBuild.awaitFinished()

            assert(result.outcome is BuildOutcome.Success)
            assert(!result.taskOutputCapturingFailed)
            assert(result.consoleOutput.contains(":execTask OUT Hello from external process"))
            assert(result.getTaskOutput(":execTask") == "Hello from external process")
        }
    }

    @Test
    fun `task output is not duplicated with one included build`() = runTest(timeout = 3.minutes) {
        testGradleProject {
            buildScript(
                """
                tasks.register("printMessage") {
                    doLast {
                        println("Hello from root task")
                    }
                }
                """.trimIndent()
            )
            includeBuild("included-lib") {
                settings("rootProject.name = \"included-lib\"")
                buildScript("")
            }
        }.use { project ->
            val projectRoot = GradleProjectRoot(project.pathString())
            val args = GradleInvocationArguments(
                additionalArguments = listOf("printMessage")
            ).withTestGradleDefaults().withInitScript("task-out")

            val runningBuild = provider.runBuild(projectRoot = projectRoot, args = args)
            val result = runningBuild.awaitFinished()

            assert(result.outcome is BuildOutcome.Success)
            assert(!result.taskOutputCapturingFailed)

            val prefixedLines = result.consoleOutput.lines()
                .filter { it.contains(":printMessage") && it.contains("Hello from root task") }
            assert(prefixedLines.size == 1) {
                "Expected exactly 1 prefixed output line but got ${prefixedLines.size}. Console output:\n${result.consoleOutput}"
            }
        }
    }

    @Test
    fun `task output is not duplicated with two included builds`() = runTest(timeout = 3.minutes) {
        testGradleProject {
            buildScript(
                """
                tasks.register("printMessage") {
                    doLast {
                        println("Hello from root task")
                    }
                }
                """.trimIndent()
            )
            includeBuild("included-lib-a") {
                settings("rootProject.name = \"included-lib-a\"")
                buildScript("")
            }
            includeBuild("included-lib-b") {
                settings("rootProject.name = \"included-lib-b\"")
                buildScript("")
            }
        }.use { project ->
            val projectRoot = GradleProjectRoot(project.pathString())
            val args = GradleInvocationArguments(
                additionalArguments = listOf("printMessage")
            ).withTestGradleDefaults().withInitScript("task-out")

            val runningBuild = provider.runBuild(projectRoot = projectRoot, args = args)
            val result = runningBuild.awaitFinished()

            assert(result.outcome is BuildOutcome.Success)
            assert(!result.taskOutputCapturingFailed)

            val prefixedLines = result.consoleOutput.lines()
                .filter { it.contains(":printMessage") && it.contains("Hello from root task") }
            assert(prefixedLines.size == 1) {
                "Expected exactly 1 prefixed output line but got ${prefixedLines.size}. Console output:\n${result.consoleOutput}"
            }
        }
    }

    @Test
    fun `taskOutputCapturingFailed is set when init script fails`() = runTest(timeout = 120.seconds) {
        testGradleProject {
            // No easy way to make the init script fail without changing it, 
            // but we can mock the failure by manually printing the warning message if we could.
            // Actually, we can use a build script that interferes or just test that if the message is there, it works.
            buildScript(
                """
                tasks.register("failInit") {
                    doLast {
                        logger.warn("Failed to set up gradle-mcp output capturing")
                    }
                }
            """.trimIndent()
            )
        }.use { project ->
            val projectRoot = GradleProjectRoot(project.pathString())
            val args = GradleInvocationArguments(
                additionalArguments = listOf("failInit")
            ).withTestGradleDefaults()

            val runningBuild = provider.runBuild(
                projectRoot = projectRoot,
                args = args
            )
            val result = runningBuild.awaitFinished()

            assert(result.outcome is BuildOutcome.Success)
            assert(result.taskOutputCapturingFailed)
        }
    }
}
