/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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.
 */

import org.scoverage.ScoverageReport
import static groovy.json.JsonOutput.*

plugins {
    id 'eclipse'
    id 'maven-publish'
    id 'org.hidetake.swagger.generator' version '2.19.2'
    id 'org.scoverage'
    id 'scala'
}

compileTestScala.options.encoding = 'UTF-8'
project.archivesBaseName = "openwhisk-tests"

scoverage {
    scoverageVersion.set("${gradle.scala.scoverageVersion}")
    scoverageScalaVersion.set("${gradle.scala.scoverageScalaVersion}")
}

def leanExcludes = [
    '**/MaxActionDurationTests*',
    'invokerShoot/**'
]

def projectsWithCoverage = [
    ':common:scala',
    ':core:controller',
    ':core:scheduler',
    ':core:invoker',
    ':tools:admin',
    ':core:cosmosdb:cache-invalidator'
]

def systemIncludes = [
            "org/apache/openwhisk/core/limits/**",
            "org/apache/openwhisk/core/admin/**",
            "org/apache/openwhisk/core/cli/test/**",
            "org/apache/openwhisk/core/apigw/actions/test/**",
            "org/apache/openwhisk/core/database/test/*CacheConcurrencyTests*",
            "org/apache/openwhisk/core/controller/test/*ControllerApiTests*",
            "org/apache/openwhisk/core/scheduler/queue/test/ElasticSearchDurationCheck*",
            "apigw/healthtests/**",
            "ha/**",
            "services/**",
            "system/basic/**",
            "system/rest/**",
]

ext.testSets = [
    "REQUIRE_ONLY_DB" : [
        "includes" : [
            "org/apache/openwhisk/**"
        ],
        "excludes" : [
            "org/apache/openwhisk/core/admin/**",
            "org/apache/openwhisk/core/apigw/actions/test/**",
            "org/apache/openwhisk/standalone/**",
            "org/apache/openwhisk/core/cli/test/**",
            "org/apache/openwhisk/core/limits/**",
            "org/apache/openwhisk/core/scheduler/**",
            "org/apache/openwhisk/core/invoker/test/*InvokerBootUpTests*",
            "org/apache/openwhisk/core/scheduler/queue/test/ElasticSearchDurationCheck*",
            "org/apache/openwhisk/common/etcd/**",
            "**/*CacheConcurrencyTests*",
            "**/*ControllerApiTests*",
            "org/apache/openwhisk/testEntities/**",
            "invokerShoot/**"
        ]
    ],
    "REQUIRE_SYSTEM" : [
        "includes" : systemIncludes,
        "excludes": [
            "org/apache/openwhisk/core/loadBalancer/test/*FPCPoolBalancerTests*",
            "system/basic/WskMultiRuntimeTests*",
            'invokerShoot/**'
        ]
    ],
    "REQUIRE_SCHEDULER" : [
        "includes" : [
            "org/apache/openwhisk/common/etcd/**",
            "org/apache/openwhisk/core/containerpool/v2/test/**",
            "org/apache/openwhisk/core/scheduler/**",
            "org/apache/openwhisk/core/invoker/test/*InvokerBootUpTests*",
            "org/apache/openwhisk/core/service/**",
        ]
    ],
    "REQUIRE_MULTI_RUNTIME" : [
        "includes" : [
            "system/basic/*MultiRuntimeTests*",
            "system/basic/*UnicodeTests*",
            "limits/**"
        ]
    ],
    "LEAN" : [
        "excludes" : leanExcludes
    ],
    "REQUIRE_SYSTEM_BASIC" : [
        "includes" : [
            "system/basic/**"
        ]
    ],
    "REQUIRE_LEAN_SYSTEM" : [
        "includes" : systemIncludes,

        // Tests suits below require Kafka so they are excluded for Lean System tests
        "excludes" : [
            "**/*KafkaConnectorTests*",
            "system/basic/WskMultiRuntimeTests*",
            "invokerShoot/**"
        ]
    ],
    "REQUIRE_STANDALONE" : [
        "includes" : [
            "org/apache/openwhisk/standalone/**"
        ]
    ]
]

testSets.each {setName, patterns ->
    def excludes = patterns["excludes"] ?: new HashSet<>()
    excludes.addAll(leanExcludes)
    patterns["excludes"] = excludes
}

//The value can be specified either via env variable
// ORG_GRADLE_PROJECT_testSetName
//Or via property -PtestSetName
if (!project.hasProperty("testSetName")) {
    ext.testSetName = "LEAN"
}

def getPattern(String name, String type) {
    def patterns = testSets[name]
    assert patterns : "No pattern found for $name"
    return patterns[type] ?: []
}

def logTestSetInfo(){
    println "Using testSet $testSetName - ${prettyPrint(toJson(testSets[testSetName]))}"
}

test {
    // Use command line option "-D testResultsDirName=<name>" to change the default test results directory ("test-results").
    // Specify a path relative to the build directory or an absolute path.
    testResultsDirName = System.getProperty('testResultsDirName', testResultsDirName)

    exclude 'invokerShoot/**'
}

task testShootInvoker(type: Test) {
    include 'invokerShoot/**'
}

task testLean(type: Test) {
    doFirst {
        logTestSetInfo()
    }
    exclude getPattern(testSetName, "excludes")
    include getPattern(testSetName, "includes")
}

task testSystemBasic(type: Test) {
    exclude getPattern("REQUIRE_SYSTEM_BASIC", "excludes")
    include getPattern("REQUIRE_SYSTEM_BASIC", "includes")
}

task testSystemKCF(type: Test) {
    exclude getPattern("REQUIRE_SYSTEM_BASIC", "excludes")
    include getPattern("REQUIRE_SYSTEM_BASIC", "includes")

    // KubernetesContainerFactory's logs API is not reliable and mixes stdout/stderr into a single stream
    exclude "**/*ActivationLogsTests*"

    // These tests fail almost all the time in openwhisk-deploy-kube TravisCI.  Disabling to unblock PRs.
    exclude "**/*WskPackageTests*"
    exclude "**/*WskSequenceTests*"
}

task testUnit(type: Test) {
    systemProperty("whisk.spi.ArtifactStoreProvider", "org.apache.openwhisk.core.database.memory.MemoryArtifactStoreProvider")
    exclude getPattern("REQUIRE_ONLY_DB", "excludes")
    include getPattern("REQUIRE_ONLY_DB", "includes")

    //Test below have direct dependency on CouchDB running
    def couchDbExcludes = [
        "**/*NamespaceBlacklistTests*",
        "**/*CleanUpActivationsTest*",
        "**/*CouchDbRestClientTests*",
        "**/*RemoveLogsTests*",
        "**/*ReplicatorTest*",
        "**/*CouchDBArtifactStoreTests*",
        "invokerShoot/**"
    ]

    exclude couchDbExcludes
}

dependencies {
    implementation "org.scala-lang:scala-library:${gradle.scala.version}"
    implementation "org.apache.commons:commons-lang3:3.3.2"
    implementation "org.apache.httpcomponents:httpclient:4.5.14:tests"
    implementation "org.apache.httpcomponents:httpmime:4.3.6"
    implementation "junit:junit:4.11"
    implementation "io.rest-assured:rest-assured:4.5.1"
    implementation "org.scalatest:scalatest_${gradle.scala.depVersion}:3.0.8"
    implementation "com.typesafe.akka:akka-testkit_${gradle.scala.depVersion}:${gradle.akka.version}"
    implementation "com.google.code.gson:gson:2.10.1"
    implementation "org.scalamock:scalamock_${gradle.scala.depVersion}:4.4.0"
    implementation "com.typesafe.akka:akka-http-testkit_${gradle.scala.depVersion}:${gradle.akka_http.version}"
    implementation "com.github.java-json-tools:json-schema-validator:2.2.8"
    implementation "org.mockito:mockito-core:2.27.0"
    implementation "io.opentracing:opentracing-mock:0.31.0"
    implementation ("org.apache.curator:curator-test:${gradle.curator.version}") {
        exclude group: 'log4j'
    }
    implementation "com.atlassian.oai:swagger-request-validator-core:1.4.5"
    constraints {
        implementation("com.atlassian.oai:swagger-request-validator-core:1.4.5")
        implementation("org.slf4j:slf4j-ext:1.7.36") {
            because 'swagger-request-validator-core cannot be upgraded to 2.x where vuln is remediated'
        }
    }
    implementation "io.github.embeddedkafka:embedded-kafka_${gradle.scala.depVersion}:2.8.1"
    constraints {
        implementation("io.github.embeddedkafka:embedded-kafka_${gradle.scala.depVersion}:2.8.1")
        implementation('org.apache.avro:avro:1.11.1') {
            because 'embeddedkafka dependency cannot be upgraded currently and avro in embedded kafka 2.4.0 has vulns'
        }
    }
    implementation "com.typesafe.akka:akka-stream-kafka-testkit_${gradle.scala.depVersion}:${gradle.akka_kafka.version}"
    implementation "com.typesafe.akka:akka-stream-testkit_${gradle.scala.depVersion}:${gradle.akka.version}"
    implementation "io.fabric8:kubernetes-server-mock:${gradle.kube_client.version}"
    implementation "org.rogach:scallop_${gradle.scala.depVersion}:3.3.2"

    implementation "com.amazonaws:aws-java-sdk-s3:1.12.395"
    implementation "com.microsoft.azure:azure-cosmos:3.7.6"
    implementation 'org.testcontainers:elasticsearch:1.17.6'
    implementation 'org.testcontainers:mongodb:1.17.1'
    implementation project(':common:scala')
    implementation project(':core:controller')
    implementation project(':core:scheduler')
    implementation project(':core:invoker')
    implementation project(':core:cosmosdb:cache-invalidator')
    implementation project(':core:monitoring:user-events')
    implementation project(':tools:admin')

    swaggerCodegen 'io.swagger:swagger-codegen-cli:2.4.29'
}

def keystorePath = new File(sourceSets.test.scala.outputDir, 'keystore')
task deleteKeystore(type: Delete) {
    delete keystorePath
}
task createKeystore(dependsOn: deleteKeystore) {
    doLast {
        def propsFile = file('../whisk.properties')
        if (propsFile.exists()) {
            Properties props = new Properties()
            props.load(new FileInputStream(propsFile))
            keystorePath.parentFile.mkdirs()
            def cmd = ['keytool', '-import', '-alias', 'Whisk', '-noprompt', '-trustcacerts', '-file', file(props['whisk.ssl.cert']), '-keystore', keystorePath, '-storepass', 'openwhisk']
            cmd.execute().waitForProcessOutput(System.out, System.err)
        }
    }
}

task buildArtifacts(type:Exec) {
  workingDir 'dat/actions'
  commandLine './build.sh'
}

tasks.withType(Test) {
    dependsOn createKeystore
    dependsOn buildArtifacts
}
createKeystore.mustRunAfter(testClasses)
buildArtifacts.mustRunAfter(testClasses)

gradle.projectsEvaluated {
    task testCoverageLean(type: Test) {
        doFirst {
            logTestSetInfo()
        }
        classpath = getScoverageClasspath(project, projectsWithCoverage)
        exclude getPattern(testSetName, "excludes")
        include getPattern(testSetName, "includes")
    }

    task testCoverage(type: Test) {
        classpath = getScoverageClasspath(project, projectsWithCoverage)
    }

    tasks.withType(ScalaCompile) {
        configure(scalaCompileOptions.forkOptions) {
            memoryMaximumSize = '1g'
        }
    }

    tasks.withType(Test) {
        systemProperties(System.getProperties())
        systemProperty("whisk.server.jar", getStandaloneJarFilePath())

        testLogging {
            events "passed", "skipped", "failed"
            showStandardStreams = true
            exceptionFormat = 'full'
        }
        maxHeapSize = "1024m" //Gradle 5.5 defaults to 512MB which is low
        outputs.upToDateWhen { false } // force tests to run every time
    }
    /**
     * Task to generate coverage xml report. Requires the
     * tests to be executed prior to its invocation
     */
    task reportCoverage(type: ScoverageReport) {
        def dependentTasks = []
        dependentTasks << copyMeasurementFiles
        projectsWithCoverage.forEach {
            dependentTasks << it + ':reportScoverage'
            dependentTasks << it + ':processScoverageResources'
        }
        dependentTasks << 'compileScoverageScala'
        dependsOn(dependentTasks)

        //Need to recreate the logic from
        //https://github.com/scoverage/gradle-scoverage/blob/924bf49a8f981f119d0604b44a782f3f8eecb359/src/main/groovy/org/scoverage/ScoveragePlugin.groovy#L137
        //default tasks retrigger the tests. As ours is a multi module integration
        //test we have to adapt the classpath and hence cannot use default reportXXXScoverage tasks
        runner = new org.scoverage.ScoverageRunner(project.configurations.scoverage)
        reportDir = reportTestScoverage.reportDir
        sources = reportTestScoverage.sources
        sourceEncoding = 'UTF-8'
        dataDir = reportTestScoverage.dataDir
        coverageOutputCobertura = reportTestScoverage.coverageOutputCobertura
        coverageOutputXML = reportTestScoverage.coverageOutputXML
        coverageOutputHTML = reportTestScoverage.coverageOutputHTML
        coverageDebug = reportTestScoverage.coverageDebug
    }
}

task copyMeasurementFiles() {
    doLast{
        Project common = project(":common:scala")
        Project controller = project(":core:controller")
        Project scheduler = project(":core:scheduler")
        Project invoker = project(":core:invoker")

        Properties wskProps = loadWhiskProps()
        String covLogsDir = wskProps.getProperty('whisk.coverage.logs.dir')
        assert covLogsDir : "Did not find coverage logs property 'whisk.coverage.logs.dir' in whisk props"

        File covLogs = new File(covLogsDir)

        copyAndRenameMeasurementFile(covLogs, 'controller', "common", common)
        copyAndRenameMeasurementFile(covLogs, 'controller', "controller", controller)
        copyAndRenameMeasurementFile(covLogs, 'scheduler', "common", common)
        copyAndRenameMeasurementFile(covLogs, 'scheduler', "scheduler", scheduler)
        copyAndRenameMeasurementFile(covLogs, 'invoker', "common", common)
        copyAndRenameMeasurementFile(covLogs, 'invoker', "invoker", invoker)
    }
}


/**
 * Scoverage measurement files are named like scoverage.measurements.xxx. Where xxx is thread id. While
 * consolidating the files between container run and normal test run we need to rename the files generated by
 * container run so that the file name becomes unique
 */
def copyAndRenameMeasurementFile(File covLogDir, String containerName, String moduleName, Project dest){
    File dir = new File(new File(covLogDir, containerName), moduleName)
    if (!dir.exists()) {
        println "Coverage logs directory ${dir.absolutePath} does not exist. Skipping measurement file collection"
        return
    }
    copy{
        from(dir)
        into("${dest.buildDir.absolutePath}/scoverage")
        rename {it+".$containerName-container"}
    }
}

def loadWhiskProps(){
    Properties p = new Properties()
    file('../whisk.properties').withInputStream {is ->
        p.load(is)
    }
    p
}

/**
 * Prepares the classpath which refer to scoverage instrumented classes from
 * dependent projects "before" the non instrumented classes
 */
def getScoverageClasspath(Project project, List<String> projectNames) {
    def combinedClasspath = projectNames.inject(project.files([])) { result, name ->
        def cp = project.project(name).sourceSets.scoverage.runtimeClasspath
        result + cp.filter {it.name.contains('scoverage')}
    }

    combinedClasspath + sourceSets.test.runtimeClasspath
}

swaggerSources {
    java {
        inputFile = file("$projectDir/../core/controller/src/main/resources/apiv1swagger.json")
        code {
            language = 'java'
            configFile = file('src/test/resources/swagger-config.json')
            templateDir = file('src/test/resources/templates')
            dependsOn validation
        }
    }
}

task testSwaggerCodegen(type: GradleBuild) {
    dependsOn swaggerSources.java.code
    buildFile = "${buildDir}/swagger-code-java/build.gradle"
    tasks = ['build']
}

def getStandaloneJarFilePath(){
    project(":core:standalone").tasks.named("bootJar").get().archiveFile.get().asFile
}
