import com.google.devtools.ksp.gradle.KspTask
import kotlinx.kover.api.KoverTaskExtension
import org.apache.tools.ant.taskdefs.optional.ReplaceRegExp
import org.jetbrains.kotlin.cli.common.toBooleanLenient

buildscript {
    dependencies {
        classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.10")
        classpath("com.google.gms:google-services:4.4.0")
    }
}

/*
 * Copyright (c) 2022 New Vector Ltd
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
    alias(libs.plugins.android.application) apply false
    alias(libs.plugins.android.library) apply false
    alias(libs.plugins.kotlin.android) apply false
    alias(libs.plugins.ksp) apply false
    alias(libs.plugins.anvil) apply false
    alias(libs.plugins.kotlin.jvm) apply false
    alias(libs.plugins.kapt) apply false
    alias(libs.plugins.dependencycheck) apply false
    alias(libs.plugins.dependencyanalysis)
    alias(libs.plugins.detekt)
    alias(libs.plugins.ktlint)
    alias(libs.plugins.dependencygraph)
    alias(libs.plugins.sonarqube)
    alias(libs.plugins.kover)
}

tasks.register<Delete>("clean").configure {
    delete(rootProject.buildDir)
}

allprojects {
    // Detekt
    apply {
        plugin("io.gitlab.arturbosch.detekt")
    }
    detekt {
        // preconfigure defaults
        buildUponDefaultConfig = true
        // activate all available (even unstable) rules.
        allRules = true
        // point to your custom config defining rules to run, overwriting default behavior
        config.from(files("$rootDir/tools/detekt/detekt.yml"))
    }
    dependencies {
        detektPlugins("io.nlopez.compose.rules:detekt:0.2.3")
    }

    // KtLint
    apply {
        plugin("org.jlleitschuh.gradle.ktlint")
    }

    // See https://github.com/JLLeitschuh/ktlint-gradle#configuration
    configure<org.jlleitschuh.gradle.ktlint.KtlintExtension> {
        // See https://github.com/pinterest/ktlint/releases/
        // TODO Regularly check for new version here ^
        version.set("0.48.2")
        android.set(true)
        ignoreFailures.set(false)
        enableExperimentalRules.set(true)
        // display the corresponding rule
        verbose.set(true)
        reporters {
            reporter(org.jlleitschuh.gradle.ktlint.reporter.ReporterType.PLAIN)
            // To have XML report for Danger
            reporter(org.jlleitschuh.gradle.ktlint.reporter.ReporterType.CHECKSTYLE)
        }
        filter {
            exclude { element -> element.file.path.contains("$buildDir/generated/") }
        }
    }
    // Dependency check
    apply {
        plugin("org.owasp.dependencycheck")
    }

    tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
        // Warnings are potential errors, so stop ignoring them
        // This is disabled by default, but the CI will enforce this.
        // You can override by passing `-PallWarningsAsErrors=true` in the command line
        // Or add a line with "allWarningsAsErrors=true" in your ~/.gradle/gradle.properties file
        kotlinOptions.allWarningsAsErrors = project.properties["allWarningsAsErrors"] == "true"
    }

    // Detect unused dependencies
    apply {
        plugin("com.autonomousapps.dependency-analysis")
    }
}

// See https://github.com/autonomousapps/dependency-analysis-android-gradle-plugin/wiki/Customizing-plugin-behavior
dependencyAnalysis {
    issues {
        all {
            onUnusedDependencies {
                exclude("com.jakewharton.timber:timber")
            }
        }
    }
}

// To run a sonar analysis:
// Run './gradlew sonar -Dsonar.login=<SONAR_LOGIN>'
// The SONAR_LOGIN is stored in passbolt as Token Sonar Cloud Bma
// Sonar result can be found here: https://sonarcloud.io/project/overview?id=vector-im_element-x-android
sonar {
    properties {
        property("sonar.projectName", "element-x-android")
        property("sonar.projectKey", "vector-im_element-x-android")
        property("sonar.host.url", "https://sonarcloud.io")
        property("sonar.projectVersion", "1.0") // TODO project(":app").android.defaultConfig.versionName)
        property("sonar.sourceEncoding", "UTF-8")
        property("sonar.links.homepage", "https://github.com/vector-im/element-x-android/")
        property("sonar.links.ci", "https://github.com/vector-im/element-x-android/actions")
        property("sonar.links.scm", "https://github.com/vector-im/element-x-android/")
        property("sonar.links.issue", "https://github.com/vector-im/element-x-android/issues")
        property("sonar.organization", "new_vector_ltd_organization")
        property("sonar.login", if (project.hasProperty("SONAR_LOGIN")) project.property("SONAR_LOGIN")!! else "invalid")

        // exclude source code from analyses separated by a colon (:)
        // Exclude Java source
        property("sonar.exclusions", "**/BugReporterMultipartBody.java")
    }
}

allprojects {
    tasks.withType<Test> {
        maxParallelForks = (Runtime.getRuntime().availableProcessors() / 2).coerceAtLeast(1)

        val isScreenshotTest = project.gradle.startParameter.taskNames.any { it.contains("paparazzi", ignoreCase = true) }
        if (isScreenshotTest) {
            // Increase heap size for screenshot tests
            maxHeapSize = "1g"
        } else {
            // Disable screenshot tests by default
            exclude("ui/S.class")
        }
    }
}

allprojects {
    apply(plugin = "kover")
}

// https://kotlin.github.io/kotlinx-kover/
// Run `./gradlew koverMergedHtmlReport` to get report at ./build/reports/kover
// Run `./gradlew koverMergedReport` to also get XML report
koverMerged {
    enable()

    filters {
        classes {
            excludes.addAll(
                listOf(
                    // Exclude generated classes.
                    "*_ModuleKt",
                    "anvil.hint.binding.io.element.*",
                    "anvil.hint.merge.*",
                    "anvil.module.*",
                    "com.airbnb.android.showkase*",
                    "io.element.android.libraries.designsystem.showkase.*",
                    "*_Factory",
                    "*_Factory$*",
                    "*_Module",
                    "*_Module$*",
                    "*Module_Provides*",
                    "Dagger*Component*",
                    "*ComposableSingletons$*",
                    "*_AssistedFactory_Impl*",
                    "*BuildConfig",
                    // Generated by Showkase
                    "*Ioelementandroid*PreviewKt$*",
                    "*Ioelementandroid*PreviewKt",
                    // Other
                    // We do not cover Nodes (normally covered by maestro, but code coverage is not computed with maestro)
                    "*Node",
                    "*Node$*",
                )
            )
        }

        annotations {
            excludes.addAll(
                listOf(
                    "*Preview",
                )
            )
        }

        projects {
            excludes.addAll(
                listOf(
                    ":anvilannotations",
                    ":anvilcodegen",
                    ":samples:minimal",
                    ":tests:testutils",
                )
            )
        }
    }

    // Run ./gradlew koverMergedVerify to check the rules.
    verify {
        // Does not seems to work, so also run the task manually on the workflow.
        onCheck.set(true)
        // General rule: minimum code coverage.
        rule {
            name = "Global minimum code coverage."
            target = kotlinx.kover.api.VerificationTarget.ALL
            bound {
                minValue = 55
                // Setting a max value, so that if coverage is bigger, it means that we have to change minValue.
                // For instance if we have minValue = 20 and maxValue = 30, and current code coverage is now 31.32%, update
                // minValue to 25 and maxValue to 35.
                maxValue = 65
                counter = kotlinx.kover.api.CounterType.INSTRUCTION
                valueType = kotlinx.kover.api.VerificationValueType.COVERED_PERCENTAGE
            }
        }
        // Rule to ensure that coverage of Presenters is sufficient.
        rule {
            name = "Check code coverage of presenters"
            target = kotlinx.kover.api.VerificationTarget.CLASS
            overrideClassFilter {
                includes += "*Presenter"
                excludes += "*Fake*Presenter"
                excludes += "io.element.android.appnav.loggedin.LoggedInPresenter$*"
                // Some options can't be tested at the moment
                excludes += "io.element.android.features.preferences.impl.developer.DeveloperSettingsPresenter$*"
            }
            bound {
                minValue = 85
                counter = kotlinx.kover.api.CounterType.INSTRUCTION
                valueType = kotlinx.kover.api.VerificationValueType.COVERED_PERCENTAGE
            }
        }
        // Rule to ensure that coverage of States is sufficient.
        rule {
            name = "Check code coverage of states"
            target = kotlinx.kover.api.VerificationTarget.CLASS
            overrideClassFilter {
                includes += "^*State$"
                excludes += "io.element.android.appnav.root.RootNavState*"
                excludes += "io.element.android.libraries.matrix.api.timeline.item.event.OtherState$*"
                excludes += "io.element.android.libraries.matrix.api.timeline.item.event.EventSendState$*"
                excludes += "io.element.android.libraries.matrix.api.room.RoomMembershipState*"
                excludes += "io.element.android.libraries.matrix.api.room.MatrixRoomMembersState*"
                excludes += "io.element.android.libraries.push.impl.notifications.NotificationState*"
                excludes += "io.element.android.features.messages.impl.media.local.pdf.PdfViewerState"
                excludes += "io.element.android.features.messages.impl.media.local.LocalMediaViewState"
                excludes += "io.element.android.features.location.impl.map.MapState*"
                excludes += "io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState*"
                excludes += "io.element.android.libraries.designsystem.swipe.SwipeableActionsState*"
                excludes += "io.element.android.features.messages.impl.timeline.components.ExpandableState*"
                excludes += "io.element.android.features.messages.impl.timeline.model.bubble.BubbleState*"
                excludes += "io.element.android.libraries.maplibre.compose.CameraPositionState*"
                excludes += "io.element.android.libraries.maplibre.compose.SaveableCameraPositionState"
                excludes += "io.element.android.libraries.maplibre.compose.SymbolState*"
                excludes += "io.element.android.features.ftue.api.state.*"
                excludes += "io.element.android.features.ftue.impl.welcome.state.*"
            }
            bound {
                minValue = 90
                counter = kotlinx.kover.api.CounterType.INSTRUCTION
                valueType = kotlinx.kover.api.VerificationValueType.COVERED_PERCENTAGE
            }
        }
        // Rule to ensure that coverage of Views is sufficient (deactivated for now).
        rule {
            name = "Check code coverage of views"
            target = kotlinx.kover.api.VerificationTarget.CLASS
            overrideClassFilter {
                includes += "*ViewKt"
            }
            bound {
                // TODO Update this value, for now there are too many missing tests.
                minValue = 0
                counter = kotlinx.kover.api.CounterType.INSTRUCTION
                valueType = kotlinx.kover.api.VerificationValueType.COVERED_PERCENTAGE
            }
        }
    }
}

// When running on the CI, run only debug test variants
val ciBuildProperty = "ci-build"
val isCiBuild = if (project.hasProperty(ciBuildProperty)) {
    val raw = project.property(ciBuildProperty) as? String
    raw?.toBooleanLenient() == true || raw?.toIntOrNull() == 1
} else {
    false
}
if (isCiBuild) {
    allprojects {
        afterEvaluate {
            tasks.withType<Test>().configureEach {
                extensions.configure<KoverTaskExtension> {
                    val enabled = name.contains("debug", ignoreCase = true)
                    isDisabled.set(!enabled)
                }
            }
        }
    }
}

// Register quality check tasks.
tasks.register("runQualityChecks") {
    project.subprojects {
        // For some reason `findByName("lint")` doesn't work
        tasks.findByPath("$path:lint")?.let { dependsOn(it) }
        tasks.findByName("detekt")?.let { dependsOn(it) }
        tasks.findByName("ktlintCheck")?.let { dependsOn(it) }
        // tasks.findByName("buildHealth")?.let { dependsOn(it) }
    }
    dependsOn(":app:knitCheck")
}

// Make sure to delete old screenshots before recording new ones
subprojects {
    val snapshotsDir = File("${project.projectDir}/src/test/snapshots")
    val removeOldScreenshotsTask = tasks.register("removeOldSnapshots") {
        onlyIf { snapshotsDir.exists() }
        doFirst {
            println("Delete previous screenshots located at $snapshotsDir\n")
            snapshotsDir.deleteRecursively()
        }
    }
    tasks.findByName("recordPaparazzi")?.dependsOn(removeOldScreenshotsTask)
    tasks.findByName("recordPaparazziDebug")?.dependsOn(removeOldScreenshotsTask)
    tasks.findByName("recordPaparazziRelease")?.dependsOn(removeOldScreenshotsTask)
}

// Workaround for https://github.com/airbnb/Showkase/issues/335
subprojects {
    tasks.withType<KspTask>() {
        doLast {
            fileTree(buildDir).apply { include("**/*ShowkaseExtension*.kt") }.files.forEach { file ->
                ReplaceRegExp().apply {
                    setMatch("^public fun Showkase.getMetadata")
                    setReplace("@Suppress(\"DEPRECATION\") public fun Showkase.getMetadata")
                    setFlags("g")
                    setByLine(true)
                    setFile(file)
                    execute()
                }
            }
        }
    }
}
