package dev.rnett.gradle.mcp.dependencies

import dev.rnett.gradle.mcp.PRINTLN
import dev.rnett.gradle.mcp.ProgressReporter
import dev.rnett.gradle.mcp.gradle.BuildManager
import dev.rnett.gradle.mcp.gradle.GradleInvocationArguments
import dev.rnett.gradle.mcp.gradle.GradleProjectRoot
import dev.rnett.gradle.mcp.gradle.GradleProvider
import dev.rnett.gradle.mcp.gradle.GradleResult
import dev.rnett.gradle.mcp.gradle.build.RunningBuild
import kotlinx.coroutines.test.runTest
import org.gradle.tooling.events.OperationType
import org.gradle.tooling.events.ProgressListener
import org.gradle.tooling.model.Model
import org.junit.jupiter.api.Test
import kotlin.reflect.KClass
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertNotNull
import kotlin.test.assertTrue

class GradleDependencyParsingTest {

    @Test
    fun `can parse structured output`() {
        val output = """
            [gradle-mcp] [DEPENDENCIES] PROJECT | : | project ':'
            [gradle-mcp] [DEPENDENCIES] REPOSITORY | : | MavenRepo | https://repo.maven.apache.org/maven2/
            [gradle-mcp] [DEPENDENCIES] SOURCESET | : | main | implementation,runtimeOnly,compileClasspath,runtimeClasspath
            [gradle-mcp] [DEPENDENCIES] CONFIGURATION | : | implementation | true | | Implementation only dependencies for source set 'main'. | false
            [gradle-mcp] [DEPENDENCIES] DEP | : | * | project : | project | : | | | | false
            [gradle-mcp] [DEPENDENCIES] DEP | : | ** | org.jetbrains.kotlin:kotlin-stdlib:1.9.22 | org.jetbrains.kotlin | kotlin-stdlib | 1.9.22 | | | false
            [gradle-mcp] [DEPENDENCIES] DEP | : | *** | org.jetbrains:annotations:13.0 | org.jetbrains | annotations | 13.0 | | | false
            [gradle-mcp] [DEPENDENCIES] CONFIGURATION | : | testImplementation | true | | Implementation only dependencies for source set 'test'. | false
            [gradle-mcp] [DEPENDENCIES] DEP | : | * | project : | project | : | | | | false
            [gradle-mcp] [DEPENDENCIES] DEP | : | ** | org.junit.jupiter:junit-jupiter:5.10.1 | org.junit.jupiter | junit-jupiter | 5.10.1 | | | false
            [gradle-mcp] [DEPENDENCIES] DEP | : | *** | org.junit.jupiter:junit-jupiter-api:5.10.1 | org.junit.jupiter | junit-jupiter-api | 5.10.1 | | | false
            [gradle-mcp] [DEPENDENCIES] DEP | : | *** | org.junit.jupiter:junit-jupiter-params:5.10.1 | org.junit.jupiter | junit-jupiter-params | 5.10.1 | | | false
            [gradle-mcp] [DEPENDENCIES] DEP | : | *** | org.junit.jupiter:junit-jupiter-engine:5.10.1 | org.junit.jupiter | junit-jupiter-engine | 5.10.1 | | | false
            [gradle-mcp] [DEPENDENCIES] DEP | : | **** | org.junit.platform:junit-platform-engine:1.10.1 | org.junit.platform | junit-platform-engine | 1.10.1 | | | false
        """.trimIndent()

        val service = DefaultGradleDependencyService(MockGradleProvider())
        val report = service.parseStructuredOutput(output)

        assertNotNull(report)
        assertEquals(1, report.projects.size)
        val project = report.projects[0]
        assertEquals(":", project.path)
        assertEquals(1, project.repositories.size)
        assertEquals("MavenRepo", project.repositories[0].name)
        assertEquals(1, project.sourceSets.size)
        assertEquals("main", project.sourceSets[0].name)
        assertEquals(2, project.configurations.size)

        val implementation = project.configurations.find { it.name == "implementation" }
        assertNotNull(implementation)
        assertEquals(1, implementation.dependencies.size)
        val projectDep = implementation.dependencies[0]
        assertEquals("project :", projectDep.id)
        assertEquals(1, projectDep.children.size)
        val stdlib = projectDep.children[0]
        assertEquals("org.jetbrains.kotlin:kotlin-stdlib:1.9.22", stdlib.id)
        assertEquals(1, stdlib.children.size)
        assertEquals("org.jetbrains:annotations:13.0", stdlib.children[0].id)
    }

    @Test
    fun `can parse already visited dependencies`() {
        val output = """
            [gradle-mcp] [DEPENDENCIES] PROJECT | : | project ':'
            [gradle-mcp] [DEPENDENCIES] CONFIGURATION | : | implementation | true | | Implementation only dependencies for source set 'main'. | false
            [gradle-mcp] [DEPENDENCIES] DEP | : | * | project : | project | : | | | | false
            [gradle-mcp] [DEPENDENCIES] DEP | : | ** | org.jetbrains.kotlin:kotlin-stdlib:1.9.22 | org.jetbrains.kotlin | kotlin-stdlib | 1.9.22 | | | false
            [gradle-mcp] [DEPENDENCIES] DEP | : | *** | org.jetbrains:annotations:13.0 | org.jetbrains | annotations | 13.0 | | | false
            [gradle-mcp] [DEPENDENCIES] DEP | : | ** | org.jetbrains:annotations:13.0 | org.jetbrains | annotations | 13.0 | | | false
        """.trimIndent()

        val service = DefaultGradleDependencyService(MockGradleProvider())
        val report = service.parseStructuredOutput(output)

        val project = report.projects[0]
        val implementation = project.configurations[0]
        val projectDep = implementation.dependencies[0]
        assertEquals(2, projectDep.children.size)

        val firstAnnotations = projectDep.children[0].children[0]
        val secondAnnotations = projectDep.children[1]

        assertEquals("org.jetbrains:annotations:13.0", firstAnnotations.id)
        assertEquals("org.jetbrains:annotations:13.0", secondAnnotations.id)
        // secondAnnotations should now have children from knownChildren
        assertEquals(0, secondAnnotations.children.size) // knownChildren for annotations:13.0 is empty as it had no children
    }

    @Test
    fun `can parse already visited dependencies with children`() {
        val output = """
            [gradle-mcp] [DEPENDENCIES] PROJECT | : | project ':'
            [gradle-mcp] [DEPENDENCIES] CONFIGURATION | : | implementation | true | | Implementation only dependencies for source set 'main'. | false
            [gradle-mcp] [DEPENDENCIES] DEP | : | * | A | group | A | 1.0 | | | false
            [gradle-mcp] [DEPENDENCIES] DEP | : | ** | B | group | B | 1.0 | | | false
            [gradle-mcp] [DEPENDENCIES] DEP | : | *** | C | group | C | 1.0 | | | false
            [gradle-mcp] [DEPENDENCIES] DEP | : | * | D | group | D | 1.0 | | | false
            [gradle-mcp] [DEPENDENCIES] DEP | : | ** | B | group | B | 1.0 | | | false
        """.trimIndent()

        val service = DefaultGradleDependencyService(MockGradleProvider())
        val report = service.parseStructuredOutput(output)

        val project = report.projects[0]
        val implementation = project.configurations[0]
        assertEquals(2, implementation.dependencies.size)

        val a = implementation.dependencies[0]
        val d = implementation.dependencies[1]

        assertEquals("A", a.id)
        assertEquals("D", d.id)

        val b1 = a.children[0]
        val b2 = d.children[0]

        assertEquals("B", b1.id)
        assertEquals("B", b2.id)

        assertEquals(1, b1.children.size)
        assertEquals("C", b1.children[0].id)

        assertEquals(1, b2.children.size)
        assertEquals("C", b2.children[0].id)
    }

    @Test
    fun `can parse interleaved output from multiple projects`() {
        val output = """
            [gradle-mcp] [DEPENDENCIES] PROJECT | :app | project ':app'
            [gradle-mcp] [DEPENDENCIES] PROJECT | :lib | project ':lib'
            [gradle-mcp] [DEPENDENCIES] CONFIGURATION | :app | implementation | true | | App dependencies | false
            [gradle-mcp] [DEPENDENCIES] CONFIGURATION | :lib | implementation | true | | Lib dependencies | false
            [gradle-mcp] [DEPENDENCIES] DEP | :app | * | :lib | project | :lib | | | | false
            [gradle-mcp] [DEPENDENCIES] DEP | :lib | * | org.jetbrains.kotlin:kotlin-stdlib:1.9.22 | org.jetbrains.kotlin | kotlin-stdlib | 1.9.22 | | | false
            [gradle-mcp] [DEPENDENCIES] DEP | :app | ** | org.jetbrains.kotlin:kotlin-stdlib:1.9.22 | org.jetbrains.kotlin | kotlin-stdlib | 1.9.22 | | | false
        """.trimIndent()

        val service = DefaultGradleDependencyService(MockGradleProvider())
        val report = service.parseStructuredOutput(output)

        assertEquals(2, report.projects.size)
        val app = report.projects.find { it.path == ":app" }!!
        val lib = report.projects.find { it.path == ":lib" }!!

        assertEquals(1, app.configurations[0].dependencies.size)
        assertEquals(1, lib.configurations[0].dependencies.size)

        val appDep = app.configurations[0].dependencies[0]
        assertEquals(":lib", appDep.id)

        val libDep = lib.configurations[0].dependencies[0]
        assertEquals("org.jetbrains.kotlin:kotlin-stdlib:1.9.22", libDep.id)

        // appDep's child (which is the stdlib) should have children from libDep
        assertEquals(1, appDep.children.size)
        val appStdLib = appDep.children[0]
        assertEquals("org.jetbrains.kotlin:kotlin-stdlib:1.9.22", appStdLib.id)
    }

    @Test
    fun `can parse buildscript source set`() {
        val output = """
            [gradle-mcp] [DEPENDENCIES] PROJECT | : | project ':'
            [gradle-mcp] [DEPENDENCIES] SOURCESET | : | buildscript | buildscript:classpath
            [gradle-mcp] [DEPENDENCIES] CONFIGURATION | : | buildscript:classpath | true | | | false
            [gradle-mcp] [DEPENDENCIES] DEP | : | * | com.google.guava:guava:32.1.2-jre | com.google.guava | guava | 32.1.2-jre | | | false
        """.trimIndent()

        val service = DefaultGradleDependencyService(MockGradleProvider())
        val report = service.parseStructuredOutput(output)

        assertNotNull(report)
        val project = report.projects[0]
        val buildscript = project.sourceSets.find { it.name == "buildscript" }
        assertNotNull(buildscript)
        assertEquals(listOf("buildscript:classpath"), buildscript.configurations)

        val config = project.configurations.find { it.name == "buildscript:classpath" }
        assertNotNull(config)
        assertEquals(1, config.dependencies.size)
        assertEquals("com.google.guava:guava:32.1.2-jre", config.dependencies[0].id)
    }

    @Test
    fun `can parse internal configurations`() {
        val output = """
            [gradle-mcp] [DEPENDENCIES] PROJECT | : | project ':'
            [gradle-mcp] [DEPENDENCIES] CONFIGURATION | : | implementation | true | | Implementation | false
            [gradle-mcp] [DEPENDENCIES] CONFIGURATION | : | jvmMainApiDependenciesMetadata | true | | Internal | true
            [gradle-mcp] [DEPENDENCIES] DEP | : | * | org.jetbrains.kotlin:kotlin-stdlib:1.9.22 | org.jetbrains.kotlin | kotlin-stdlib | 1.9.22 | | | false
        """.trimIndent()

        val service = DefaultGradleDependencyService(MockGradleProvider())
        val report = service.parseStructuredOutput(output)

        val project = report.projects[0]
        assertEquals(2, project.configurations.size)

        val normal = project.configurations.find { it.name == "implementation" }!!
        assertFalse(normal.isInternal)

        val internal = project.configurations.find { it.name == "jvmMainApiDependenciesMetadata" }!!
        assertTrue(internal.isInternal)
    }

    @Test
    fun `can parse note containing escaped pipe`() {
        val output = """
            [gradle-mcp] [DEPENDENCIES] NOTE | : | update check regex: ^foo\|bar$
        """.trimIndent()

        val service = DefaultGradleDependencyService(MockGradleProvider())
        val report = service.parseStructuredOutput(output)

        assertEquals(listOf("update check regex: ^foo|bar$"), report.notes)
    }

    @Test
    fun `can parse escaped pipe in repository field`() {
        val output = """
            [gradle-mcp] [DEPENDENCIES] PROJECT | : | project ':'
            [gradle-mcp] [DEPENDENCIES] REPOSITORY | : | Releases\|Snapshots | https://repo.maven.apache.org/maven2/
        """.trimIndent()

        val service = DefaultGradleDependencyService(MockGradleProvider())
        val report = service.parseStructuredOutput(output)

        assertEquals("Releases|Snapshots", report.projects.single().repositories.single().name)
    }

    @Test
    fun `getDependencies retains non-matching ancestors of matching dependencies`() = runTest {
        val output = """
            [gradle-mcp] [DEPENDENCIES] PROJECT | : | project ':'
            [gradle-mcp] [DEPENDENCIES] CONFIGURATION | : | implementation | true | | Implementation | false
            [gradle-mcp] [DEPENDENCIES] DEP | : | * | parent:container:1.0 | parent | container | 1.0 | | | false
            [gradle-mcp] [DEPENDENCIES] DEP | : | ** | target:match:1.0 | target | match | 1.0 | | 2.0 | false
            [gradle-mcp] [DEPENDENCIES] DEP | : | ** | other:drop:1.0 | other | drop | 1.0 | | 2.0 | false
            [gradle-mcp] [DEPENDENCIES] DEP | : | * | other:root:1.0 | other | root | 1.0 | | 2.0 | false
        """.trimIndent()

        val provider = object : MockGradleProvider() {
            override fun runBuild(
                projectRoot: GradleProjectRoot,
                args: GradleInvocationArguments,
                additionalProgressListeners: Map<ProgressListener, Set<OperationType>>,
                stdoutLineHandler: ((String) -> Unit)?,
                stderrLineHandler: ((String) -> Unit)?,
                progress: ProgressReporter
            ): RunningBuild {
                val runningBuild = RunningBuild(
                    id = dev.rnett.gradle.mcp.gradle.BuildId("test"),
                    args = args,
                    startTime = kotlin.time.Clock.System.now(),
                    projectRoot = java.nio.file.Path.of(""),
                    cancellationTokenSource = org.gradle.tooling.GradleConnector.newCancellationTokenSource(),
                    scope = this@runTest
                )
                runningBuild.logBuffer.append(output)
                runningBuild.finish(null) {}
                return runningBuild
            }
        }

        val service = DefaultGradleDependencyService(provider)

        with(ProgressReporter.PRINTLN) {
            val report = service.getDependencies(
                GradleProjectRoot(""),
                options = DependencyRequestOptions(
                    dependency = "^target:match:1\\.0$",
                    versionFilter = "^2\\.0$"
                )
            )
            val dependencies = report.projects.single().configurations.single().dependencies

            assertEquals(listOf("parent:container:1.0"), dependencies.map { it.id })
            assertEquals(listOf("target:match:1.0"), dependencies.single().children.map { it.id })
        }
    }

    @Test
    fun `getDependencies filters internal configurations by default`() = runTest {
        val output = """
            [gradle-mcp] [DEPENDENCIES] PROJECT | : | project ':'
            [gradle-mcp] [DEPENDENCIES] CONFIGURATION | : | implementation | true | | Implementation | false
            [gradle-mcp] [DEPENDENCIES] DEP | : | * | a:a:1.0 | a | a | 1.0 | | | false
            [gradle-mcp] [DEPENDENCIES] CONFIGURATION | : | jvmMainApiDependenciesMetadata | true | | Internal | true
            [gradle-mcp] [DEPENDENCIES] DEP | : | * | b:b:1.0 | b | b | 1.0 | | | false
        """.trimIndent()

        val provider = object : MockGradleProvider() {
            override fun runBuild(
                projectRoot: GradleProjectRoot,
                args: GradleInvocationArguments,
                additionalProgressListeners: Map<ProgressListener, Set<OperationType>>,
                stdoutLineHandler: ((String) -> Unit)?,
                stderrLineHandler: ((String) -> Unit)?,
                progress: ProgressReporter
            ): RunningBuild {
                val rb = RunningBuild(
                    id = dev.rnett.gradle.mcp.gradle.BuildId("test"),
                    args = args,
                    startTime = kotlin.time.Clock.System.now(),
                    projectRoot = java.nio.file.Path.of(""),
                    cancellationTokenSource = org.gradle.tooling.GradleConnector.newCancellationTokenSource(),
                    scope = this@runTest
                )
                rb.logBuffer.append(output)
                rb.finish(null) {}
                return rb
            }
        }

        val service = DefaultGradleDependencyService(provider)

        with(ProgressReporter.PRINTLN) {
            val report = service.getDependencies(GradleProjectRoot(""), options = DependencyRequestOptions(includeInternal = false))
            val project = report.projects[0]
            assertEquals(1, project.configurations.size)
            assertEquals("implementation", project.configurations[0].name)

            val allDeps = project.allDependencies().map { it.id }.toList()
            assertTrue("a:a:1.0" in allDeps)
            assertFalse("b:b:1.0" in allDeps)

            val reportWithInternal = service.getDependencies(GradleProjectRoot(""), options = DependencyRequestOptions(includeInternal = true))
            val projectWithInternal = reportWithInternal.projects[0]
            assertEquals(2, projectWithInternal.configurations.size)
            assertTrue(projectWithInternal.allDependencies().any { it.id == "b:b:1.0" })
        }
    }

    private open class MockGradleProvider : GradleProvider {
        override suspend fun <T : Model> getBuildModel(
            projectRoot: GradleProjectRoot,
            kClass: KClass<T>,
            args: GradleInvocationArguments,
            additionalProgressListeners: Map<ProgressListener, Set<OperationType>>,
            stdoutLineHandler: ((String) -> Unit)?,
            stderrLineHandler: ((String) -> Unit)?,
            progress: ProgressReporter,
            requiresGradleProject: Boolean
        ): GradleResult<T> = throw UnsupportedOperationException()

        override fun runBuild(
            projectRoot: GradleProjectRoot,
            args: GradleInvocationArguments,
            additionalProgressListeners: Map<ProgressListener, Set<OperationType>>,
            stdoutLineHandler: ((String) -> Unit)?,
            stderrLineHandler: ((String) -> Unit)?,
            progress: ProgressReporter
        ): RunningBuild = throw UnsupportedOperationException()

        override fun runTests(
            projectRoot: GradleProjectRoot,
            testPatterns: Map<String, Set<String>>,
            args: GradleInvocationArguments,
            additionalProgressListeners: Map<ProgressListener, Set<OperationType>>,
            stdoutLineHandler: ((String) -> Unit)?,
            stderrLineHandler: ((String) -> Unit)?,
            progress: ProgressReporter
        ): RunningBuild = throw UnsupportedOperationException()

        override val buildManager: BuildManager
            get() = throw UnsupportedOperationException()

        override fun close() {}
    }
}
