/*
 * Copyright 2010-2022 JetBrains s.r.o. and Kotlin Programming Language contributors.
 * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
 */

@file:Suppress("FunctionName")

package org.jetbrains.kotlin.gradle.unitTests

import org.gradle.api.attributes.Attribute
import org.gradle.api.attributes.AttributeContainer
import org.gradle.api.component.ComponentWithVariants
import org.gradle.api.component.SoftwareComponent
import org.gradle.api.internal.component.SoftwareComponentInternal
import org.gradle.api.internal.project.ProjectInternal
import org.gradle.api.plugins.JavaPluginExtension
import org.gradle.api.publish.PublishingExtension
import org.gradle.api.publish.maven.MavenPublication
import org.gradle.api.publish.maven.plugins.MavenPublishPlugin
import org.gradle.kotlin.dsl.apply
import org.gradle.testfixtures.ProjectBuilder
import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
import org.jetbrains.kotlin.gradle.plugin.mpp.internal
import org.jetbrains.kotlin.gradle.util.*
import org.jetbrains.kotlin.gradle.utils.createConsumable
import org.jetbrains.kotlin.util.capitalizeDecapitalize.toLowerCaseAsciiOnly
import kotlin.test.*

class MppPublicationTest {

    private val project = ProjectBuilder.builder().build() as ProjectInternal

    init {
        project.plugins.apply("kotlin-multiplatform")
        project.plugins.apply("maven-publish")
    }

    private val kotlin = project.extensions.getByType(KotlinMultiplatformExtension::class.java)

    init {
        kotlin.jvm()
        kotlin.js().nodejs()
    }

    @Test
    fun `contains kotlinMultiplatform publication`() {
        project.evaluate()
        val publishing = project.extensions.getByType(PublishingExtension::class.java)
        publishing.publications
            .withType(MavenPublication::class.java)
            .findByName("kotlinMultiplatform") ?: fail("Missing 'kotlinMultiplatform' publication")
    }


    @Test
    fun `all publication contains sourcesJar`() {
        project.evaluate()
        val publishing = project.extensions.getByType(PublishingExtension::class.java)
        publishing.publications
            .filterIsInstance<MavenPublication>()
            .forEach { publication ->
                val sources = publication.artifacts.filter { artifact -> artifact.classifier == "sources" }
                assertTrue(sources.isNotEmpty(), "Expected at least one sources artifact for ${publication.name}")
            }
    }

    @Test
    fun `jvm sources elements has same attributes as java sources elements from default java gradle plugin`() {
        val javaProject = ProjectBuilder.builder().build().run {
            // Given
            plugins.apply("java")
            plugins.apply("maven-publish")

            extensions.getByType(JavaPluginExtension::class.java).run {
                withSourcesJar()
            }

            // When
            this as ProjectInternal
            this.evaluate()
        }

        val javaSourcesElementsAttributes = javaProject
            .components
            .getByName("java")
            .attributesOfUsageContext("sourcesElements")

        project.evaluate()

        val kotlinComponent = project.components.getByName("kotlin") as ComponentWithVariants
        val jvmComponent = kotlinComponent.variants.first { it.name == "jvm" }
        val jvmSourcesElementsAttributes = jvmComponent.attributesOfUsageContext("jvmSourcesElements-published")

        val extraExpectedAttributes = mapOf(               // target disambiguation attribute
            "org.jetbrains.kotlin.platform.type" to "jvm", // is set by default by kgp, when it sets usage as `java-runtime-jars`
            "org.gradle.libraryelements" to "jar",         // see [KotlinUsages.producerApiUsage]
            "org.gradle.jvm.environment" to "standard-jvm"
        )

        val expectedAttributes = javaSourcesElementsAttributes.toMapOfStrings() + extraExpectedAttributes
        val actualAttributes = jvmSourcesElementsAttributes.toMapOfStrings()
        assertEquals(expectedAttributes, actualAttributes)
    }

    @Test
    fun `sources elements includes user-defined attributes`() {
        val userAttribute = Attribute.of("userAttribute", String::class.java)
        kotlin.run {
            targets.all { target -> target.attributes { attribute(userAttribute, target.name) } }
            js(IR)
            linuxX64()
            iosX64()
        }

        project.evaluate()

        val kotlinComponent = project.components.getByName("kotlin") as ComponentWithVariants
        val sourcesElements = listOf("jvm", "js", "linuxX64", "iosX64")
            .map { targetName ->
                targetName to kotlinComponent.variants
                    .first { it.name == targetName }
                    .attributesOfUsageContext("${targetName}SourcesElements-published")
            }

        for ((targetName, sourceElements) in sourcesElements) {
            assertTrue(
                message = "$sourceElements of target $targetName doesn't have 'userAttribute'"
            ) { sourceElements.attributes.toMapOfStrings().containsKey("userAttribute") }
        }
    }

    @Test
    fun `sources elements should not have any dependencies`() {
        project.evaluate()
        project.configurations
            .filter { it.name.toLowerCaseAsciiOnly().contains("sourceselements") }
            .forEach { configuration ->
                if (configuration.dependencies.isNotEmpty()) fail("Configuration $configuration should not have dependencies")
            }
    }


    @Test
    fun `sourcesJar task should be available during configuration time`() {
        kotlin.linuxX64("linux")
        project.evaluate()

        val sourcesJars = listOf(
            "sourcesJar", // sources of common source sets i.e. root module
            "jvmSourcesJar",
            "jsSourcesJar",
            "linuxSourcesJar"
        )

        for (sourcesJarTaskName in sourcesJars) {
            val sourcesJar = project.tasks.findByName(sourcesJarTaskName)
            assertNotNull(sourcesJar, "Task '$sourcesJarTaskName' should exist during project configuration time")
        }
    }

    @Test
    fun `test withSourcesJar DSL on extension level`() {
        kotlin.linuxX64()

        kotlin.withSourcesJar(publish = false)

        for (target in kotlin.targets) {
            assertFalse(target.internal.isSourcesPublishable)
        }
    }

    @Test
    fun `test withSourcesJar DSL on target level`() {
        kotlin.linuxX64("linux") {
            withSourcesJar(publish = false)
        }

        for (target in kotlin.targets) {
            if (target.name == "linux") {
                assertFalse(target.internal.isSourcesPublishable)
            } else {
                assertTrue(target.internal.isSourcesPublishable)
            }
        }
    }

    @Test
    fun `test that sourcesJar tasks still exist even if sources should not be published`() {
        kotlin.linuxX64("linux")
        kotlin.withSourcesJar(publish = false)
        project.evaluate()

        val sourcesJars = listOf(
            "sourcesJar", // sources of common source sets i.e. root module
            "jvmSourcesJar",
            "jsSourcesJar",
            "linuxSourcesJar"
        )

        for (sourcesJarTaskName in sourcesJars) {
            val sourcesJar = project.tasks.findByName(sourcesJarTaskName)
            assertNotNull(sourcesJar, "Task '$sourcesJarTaskName' should exist during project configuration time")
        }
    }

    @Test
    fun `test that no sourcesElements should be consumable when sources are published`() {
        kotlin.linuxX64("linux")
        kotlin.withSourcesJar(publish = true)

        project.evaluate()
        kotlin.targets.forEach {
            val sourcesElements = project.configurations.getByName(it.sourcesElementsConfigurationName)
            if (!sourcesElements.isCanBeConsumed) fail("Configuration '${it.sourcesElementsConfigurationName}' should be consumable")
        }
    }

    @Test
    fun `test that no sourcesElements should be consumable when sources are not published`() {
        kotlin.linuxX64("linux")
        kotlin.withSourcesJar(publish = false)

        project.evaluate()
        kotlin.targets.forEach {
            val sourcesElements = project.configurations.getByName(it.sourcesElementsConfigurationName)
            if (sourcesElements.isCanBeConsumed) fail("Configuration '${it.sourcesElementsConfigurationName}' should not be consumable")
        }
    }

    @Test
    fun `test creation of default publications can be disabled`() {
        val project = buildProject { }
        project.propertiesExtension.set("kotlin.internal.mpp.createDefaultMultiplatformPublications", "false")
        project.plugins.apply("kotlin-multiplatform")
        project.plugins.apply("maven-publish")

        project.kotlin {
            jvm()
            linuxArm64()
        }

        project.evaluate()

        val publishing = project.extensions.getByType(PublishingExtension::class.java)
        val actualPublications = publishing.publications.names
        if (actualPublications.isNotEmpty()) fail("Expected no default publications to be created")

        // Even though creation of default publications are disabled
        // it is still important that components and everything else needed for publishing are still present
        val components = project.components.names
        assertEquals(setOf("kotlin", "adhocKotlin"), components)

        val expectedSourcesJarTasks = setOf("sourcesJar", "metadataSourcesJar", "jvmSourcesJar", "linuxArm64SourcesJar")
        val missingSourcesJarTasks = expectedSourcesJarTasks - project.tasks.names
        if (missingSourcesJarTasks.isNotEmpty()) fail("Following sources jar tasks are not created: $missingSourcesJarTasks")

        val expectedConfigurations = setOf(
            "jvmApiElements",
            "jvmApiElements-published",
            "jvmRuntimeElements",
            "jvmRuntimeElements-published",
            "jvmSourcesElements",
            "jvmSourcesElements-published",
            "metadataApiElements",
        )
        val missingConfigurations = expectedConfigurations - project.configurations.names
        if (missingConfigurations.isNotEmpty()) fail("Following configurations are not created: $missingConfigurations")
    }

    @Test
    @OptIn(ExperimentalWasmDsl::class)
    fun `test MavenPublish applied before KotlinMultiplatform KT-28520`() {
        val project = buildProject {

            apply<MavenPublishPlugin>()

            assertTrue(pluginManager.hasPlugin("maven-publish"), "Expected project has MavenPublish plugin")
            assertFalse(project.pluginManager.hasPlugin("kotlin-multiplatform"), "Expected project does not have KGP")

            applyMultiplatformPlugin()

            assertTrue(project.pluginManager.hasPlugin("kotlin-multiplatform"), "Expected project has KGP")

            kotlin {
                jvm()

                linuxX64()
                mingwX64()
                macosX64()

                js { browser() }

                wasmJs { browser() }
                wasmWasi { nodejs() }
            }
        }.evaluate()

        assertTrue(project.pluginManager.hasPlugin("maven-publish"), "Expected evaluated project has MavenPublish plugin")
        assertTrue(project.pluginManager.hasPlugin("kotlin-multiplatform"), "Expected evaluated project has KGP")
    }

    private fun SoftwareComponent.attributesOfUsageContext(usageContextName: String): AttributeContainer {
        this as SoftwareComponentInternal
        return usages.first { it.name == usageContextName }.attributes
    }

    private fun AttributeContainer.toMapOfStrings(): Map<String, String> = keySet()
        .associate { key -> key.name to getAttribute(key).toString() }
}
