import groovy.transform.Memoized
import groovy.xml.XmlParser

apply plugin: 'jacoco'

jacoco {
    toolVersion = libs.versions.jacoco.get()

}

android {
    testCoverage {
        jacocoVersion = libs.versions.jacoco.get()
    }
}

@Memoized
Properties getLocalProperties() {
    final propertiesFile = project.rootProject.file("local.properties")

    final properties = new Properties()

    if (propertiesFile.exists()) {
        properties.load(propertiesFile.newDataInputStream())
    }

    return properties
}

def openReport(htmlOutDir) {
    final reportPath = "$htmlOutDir/index.html"

    println "HTML Report: file://$reportPath"

    if (!project.hasProperty("open-report")) {
        println "to open the report automatically in your default browser add '-Popen-report' cli argument"
        return
    }

    def os = org.gradle.internal.os.OperatingSystem.current()
    if (os.isWindows()) {
        new ProcessBuilder('cmd', '/c', "start $reportPath")
    } else if (os.isMacOsX()) {
        new ProcessBuilder('open', "$reportPath").start()
    } else if (os.isLinux()) {
        try {
            new ProcessBuilder('xdg-open', "$reportPath" )
        } catch (Exception ignored) {
            if (localProperties.containsKey("linux-html-cmd")) {
                new ProcessBuilder(properties.get("linux-html-cmd"), "$reportPath")
            } else {
                println "'linux-html-cmd' property could not be found in 'local.properties'"
            }
        }
    }
}

tasks.withType(Test).configureEach {
    jacoco.includeNoLocationClasses = true
    jacoco.excludes = ['jdk.internal.*']
}

// source: https://medium.com/jamf-engineering/android-kotlin-code-coverage-with-jacoco-sonar-and-gradle-plugin-6-x-3933ed503a6e
def fileFilter = [
        // android
        '**/R.class',
        '**/R$*.class',
        '**/BuildConfig.*',
        '**/Manifest*.*',
        '**/*Test*.*',
        'android/**/*.*',
        // kotlin
        '**/*MapperImpl*.*',
        '**/*$ViewInjector*.*',
        '**/*$ViewBinder*.*',
        '**/BuildConfig.*',
        '**/*Component*.*',
        '**/*BR*.*',
        '**/Manifest*.*',
        '**/*$Lambda$*.*',
        '**/*Companion*.*',
        '**/*Module*.*',
        '**/*Dagger*.*',
        '**/*Hilt*.*',
        '**/*MembersInjector*.*',
        '**/*_MembersInjector.class',
        '**/*_Factory*.*',
        '**/*_Provide*Factory*.*',
        '**/*Extensions*.*',
        // sealed and data classes
        '**/*$Result.*',
        '**/*$Result$*.*'
]

def flavorClassDir = 'tmp/kotlin-classes/play'
def moduleClassDir = 'tmp/kotlin-classes/'
if (rootProject.testReleaseBuild) {
    flavorClassDir += "Release"
    moduleClassDir += "release"
} else {
    flavorClassDir += "Debug"
    moduleClassDir += "debug"
}

// Our merge report task
tasks.register('jacocoTestReport', JacocoReport) {
    def htmlOutDir = project.layout.buildDirectory.dir("reports/jacoco/$name/html").get().asFile

    doLast {
        openReport htmlOutDir
    }

    reports {
        xml.required = true
        html.outputLocation = htmlOutDir
    }

    def kotlinClasses = fileTree(dir: project.layout.buildDirectory.dir(flavorClassDir), excludes: fileFilter)
    def mainSrc = "$project.projectDir/src/main/java"

    sourceDirectories.from = files([mainSrc])
    classDirectories.from = files([kotlinClasses])
    executionData.from = fileTree(dir: project.layout.buildDirectory, includes: [
            '**/*.exec',
            '**/*.ec'
    ])
    dependsOn('testPlayDebugUnitTest')
    dependsOn("connectedPlay${rootProject.androidTestVariantName}AndroidTest")
}

// modules do not yet support flavors (play/full)
def modulesToUnitTest = [":common", ":libanki"]

// A unit-test only report task
tasks.register('jacocoUnitTestReport', JacocoReport) {report ->
    def htmlOutDir = layout.buildDirectory.dir("reports/jacoco/$name/html").get().asFile

    doLast {
        openReport htmlOutDir
    }

    reports {
        xml.required = true
        html.outputLocation = htmlOutDir
    }

    includeUnitTestCoverage(report, getProject(), flavorClassDir)
    dependsOn('testPlayDebugUnitTest')

    for (module in modulesToUnitTest) {
        includeUnitTestCoverage(report, project(module), moduleClassDir)
        dependsOn("${module}:testDebugUnitTest")
    }
}

gradle.projectsEvaluated {
    // ensure test results aren't cached
    tasks.named('testPlayDebugUnitTest') {task ->
        task.outputs.upToDateWhen { false }
        task.outputs.cacheIf { false }
    }
    for (module in modulesToUnitTest) {
        project(module).tasks.named('testDebugUnitTest')?.configure {task ->
            task.outputs.upToDateWhen { false }
            task.outputs.cacheIf { false }
        }
    }
}



ext.includeUnitTestCoverage = { JacocoReport report, Project project, String classDir ->
    report.sourceDirectories.from.add(files("${project.projectDir}/src/main/java"))
    report.classDirectories.from.add(files(
            fileTree(dir: project.layout.buildDirectory.dir(classDir), excludes: fileFilter))
    )
    report.executionData.from.add(files(
            fileTree(dir: project.layout.buildDirectory, includes: [
                    '**/*.exec',
            ])
    ))
}

// A connected android tests only report task
tasks.register('jacocoAndroidTestReport', JacocoReport) {
    def htmlOutDir = layout.buildDirectory.dir("reports/jacoco/$name/html").get().asFile

    doLast {
        openReport htmlOutDir
    }

    reports {
        xml.required = true
        html.outputLocation = htmlOutDir
    }

    def kotlinClasses = fileTree(dir: project.layout.buildDirectory.dir(flavorClassDir), excludes: fileFilter)
    def mainSrc = "$project.projectDir/src/main/java"

    sourceDirectories.from = files([mainSrc])
    classDirectories.from = files([kotlinClasses])
    executionData.from = fileTree(dir: project.layout.buildDirectory, includes: [
            '**/*.ec'
    ])
    dependsOn("connectedPlay${rootProject.androidTestVariantName}AndroidTest")
}

// Issue 16640 - some emulators run, but register zero coverage
tasks.register('assertNonzeroAndroidTestCoverage') {
    doLast {
        File jacocoReport = layout.buildDirectory.dir("reports/jacoco/jacocoAndroidTestReport/jacocoAndroidTestReport.xml").get().asFile

        if (!jacocoReport.exists())
            throw new FileNotFoundException("jacocoAndroidTestReport.xml was not found after running jacocoAndroidTestReport")

        XmlParser xmlParser = new XmlParser()
        xmlParser.setFeature("http://apache.org/xml/features/disallow-doctype-decl", false)
        xmlParser.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false)

        Node reportRoot = xmlParser.parse(jacocoReport)
        boolean hasCovered = false

        // https://github.com/jacoco/jacoco/blob/5aabb2eb60bbcd05df968005f1746ba19dcd5361/org.jacoco.report/src/org/jacoco/report/internal/xml/ReportElement.java#L190
        for (Node child : reportRoot.children()) {
            if (child.name() != "counter")
                continue

            if (child.attribute("covered") == "0") {
                logger.warn("jacoco registered zero code coverage for counter type " + child.attribute("type"), null)
            } else {
                hasCovered = true
            }
        }

        if (!hasCovered)
            throw new GradleScriptException("androidTest registered zero code coverage in jacocoAndroidTestReport.xml. Probably some incompatibilities in the toolchain.", null)
    }
}
afterEvaluate {
    tasks.named('jacocoAndroidTestReport').configure { finalizedBy('assertNonzeroAndroidTestCoverage') }
}
