/*
 * 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.
 */

//
// Configure test randomization seeds and derived test properties.
//

import java.nio.file.*
import com.carrotsearch.randomizedtesting.SeedUtils
import com.carrotsearch.randomizedtesting.generators.RandomPicks
import org.apache.tools.ant.types.Commandline

buildscript {
  repositories {
    mavenCentral()
  }

  dependencies {
    classpath libs.carrotsearch.randomizedtesting.runner
  }
}

class SecurityArgumentProvider implements CommandLineArgumentProvider {
  @Internal
  Directory commonSolrDir

  @InputFile
  @PathSensitive(PathSensitivity.RELATIVE)
  RegularFile javaSecurityPolicy

  @Override
  Iterable<String> asArguments() {
    [
        "-Dcommon-solr.dir=${commonSolrDir.getAsFile()}",
        "-Djava.security.policy=${javaSecurityPolicy.getAsFile()}"
    ]
  }
}

def resources = scriptResources(buildscript)

// Pick the "root" seed from which everything else is derived.
configure(rootProject) {
  ext {
    rootSeed = propertyOrDefault('tests.seed', String.format("%08X", new Random().nextLong()))
    rootSeedUserProvided = (propertyOrDefault('tests.seed', null) != null)
    rootSeedLong = SeedUtils.parseSeedChain(rootSeed)[0]
    projectSeedLong = rootSeedLong ^ project.path.hashCode()
  }

  task randomizationInfo() {
    doFirst {
      logger.lifecycle("Running tests with randomization seed: tests.seed=${rootSeed}")
    }
  }
}

// Any test task will trigger display of randomization settings.
allprojects {
  tasks.withType(Test) { task ->
    task.dependsOn rootProject.randomizationInfo
  }
}

// Configure test property defaults and their descriptions.
allprojects {
  plugins.withType(JavaPlugin) {
    project.ext {
      testOptions += [
          // seed, repetition and amplification.
          [propName: 'tests.seed', value: { -> rootSeed }, description: "Sets the master randomization seed."],
          [propName: 'tests.iters', value: null, description: "Duplicate (re-run) each test case N times."],
          [propName: 'tests.multiplier', value: 1, description: "Value multiplier for randomized tests."],
          [propName: 'tests.maxfailures', value: null, description: "Skip tests after a given number of failures."],
          [propName: 'tests.timeoutSuite', value: null, description: "Timeout (in millis) for an entire suite."],
          [propName: 'tests.failfast', value: "false", description: "Stop the build early on failure.", buildOnly: true],
          // asserts, debug output.
          [propName: 'tests.asserts', value: "true", description: "Enables or disables assertions mode."],
          [propName: 'tests.infostream', value: false, description: "Enables or disables infostream logs."],
          [propName: 'tests.leaveTemporary', value: false, description: "Leave temporary directories after tests complete."],
          [propName: 'tests.useSecurityManager',
           value: { -> rootProject.ext.runtimeJavaVersion <= JavaVersion.VERSION_23 ? 'true' : 'false' },
           description: "Control security manager in tests.", buildOnly: true],
          // component randomization
          [propName: 'tests.codec', value: "random", description: "Sets the codec tests should run with."],
          [propName: 'tests.directory', value: "random", description: "Sets the Directory implementation tests should run with."],
          [propName: 'tests.postingsformat', value: "random", description: "Sets the postings format tests should run with."],
          [propName: 'tests.docvaluesformat', value: "random", description: "Sets the doc values format tests should run with."],
          [propName: 'tests.locale', value: "random", description: "Sets the default locale tests should run with."],
          [propName: 'tests.timezone', value: "random", description: "Sets the default time zone tests should run with."],
          // filtering
          [propName: 'tests.filter', value: null, description: "Applies a test filter (see :helpTests)."],
          [propName: 'tests.nightly', value: false, description: "Enables or disables @Nightly tests."],
          [propName: 'tests.weekly', value: false, description: "Enables or disables @Weekly tests."],
          [propName: 'tests.monster', value: false, description: "Enables or disables @Monster tests."],
          [propName: 'tests.awaitsfix', value: null, description: "Enables or disables @AwaitsFix tests."],
          [propName: 'tests.badapples', value: null, description: "Enables or disables @BadApple tests."],
          [propName: 'tests.file.encoding',
           value: { ->
             RandomPicks.randomFrom(new Random(projectSeedLong), ["US-ASCII", "ISO-8859-1", "UTF-8"])
           },
           description: "Sets the default file.encoding on test JVM.", buildOnly: true],
          // test data
          [propName: 'tests.linedocsfile', value: 'europarl.lines.txt.gz', description: "Test data file path."],
          // miscellaneous; some of them very weird.
          [propName: 'tests.bwcdir', value: null, description: "Data for backward-compatibility indexes."],
      ]
    }
  }
}

// Add Solr-specific test configs settings.
configure(allprojects.findAll {project -> project.path.startsWith(":solr") }) {
  plugins.withType(JavaPlugin) {
    project.ext {
      testOptions += [
          [propName: 'tests.src.home', value: null, description: "See SOLR-14023."],
          [propName: 'solr.tests.use.numeric.points', value: null, description: "Point implementation to use (true=numerics, false=trie)."],
      ]
    }
  }
}

// Resolve test option values after all evaluation is complete.
allprojects {
  plugins.withType(JavaPlugin) {
    afterEvaluate {
      project.ext.testOptionsResolved = testOptions.findAll { opt ->
        propertyOrDefault(opt.propName, opt.value) != null
      }.collectEntries { opt ->
        [(opt.propName): Objects.toString(resolvedTestOption(opt.propName))]
      }

      // Compute the "reproduce with" string.
      project.ext.testOptionsForReproduceLine = testOptions.findAll { opt ->
        if (opt["includeInReproLine"] == false) {
          return false
        }

        def defValue = Objects.toString(opt.value, null)
        def value = testOptionsResolved[opt.propName]
        return defValue != value
      }.collect { opt ->
        Commandline.quoteArgument("-P" + opt.propName + "=" + testOptionsResolved[opt.propName])
      }.join(" ")

      // leaving temporary folder option has multiple aliases...
      if ([
         "tests.leaveTemporary",
         "tests.leavetemporary",
         "tests.leavetmpdir",
         "solr.test.leavetmpdir",
      ].find { prop ->
        def v = Boolean.parseBoolean(propertyOrDefault(prop, "false"))
        if (v) {
          logger.lifecycle("Update your code to use the official 'tests.leaveTemporary' option (you used '${prop}').")
        }
        return v
      }) {
        testOptionsResolved['tests.leaveTemporary'] = "true"
      }

      // Append resolved test properties to the test task.
      tasks.withType(Test) { task ->
        // TODO: we could remove some options that are only relevant to the build environment
        // and not the test JVM itself.
        systemProperties testOptionsResolved

        if (Boolean.parseBoolean(testOptionsResolved['tests.asserts'])) {
          jvmArgs("-ea", "-esa")
        } else {
          enableAssertions = false
        }

        if (Boolean.parseBoolean(testOptionsResolved["tests.failfast"])) {
          failFast true
        }

        // The Lucene version is only available after resolving was done, so add lately using a provider:
        jvmArgumentProviders.add({
          def luceneMatchVersion = rootProject.luceneBaseVersionProvider.get()
          return [
            "-Dtests.LUCENE_VERSION=${luceneMatchVersion}",
            "-Dtests.luceneMatchVersion=${luceneMatchVersion}",
          ]
        } as CommandLineArgumentProvider)

        // Enable security manager, if requested. We could move the selection of security manager and security policy
        // to each project's build/ configuration but it seems compact enough to keep it here for now.
        def useSecurityManager = Boolean.parseBoolean(testOptionsResolved["tests.useSecurityManager"]);
        // Allow the project to override this
        if (project.ext.has("useSecurityManager")) {
          useSecurityManager &= project.ext.get("useSecurityManager")
        }
        if (useSecurityManager) {
          def commonSolrDir = project(':solr').layout.projectDirectory
          def javaSecurityPolicy = layout.projectDirectory.file("${resources}/policies/solr-tests.policy")
          jvmArgumentProviders.add(
              new SecurityArgumentProvider(
                  commonSolrDir: commonSolrDir,
                  javaSecurityPolicy: javaSecurityPolicy
              )
          )
          systemProperty 'java.security.manager', "default"

          def gradleUserHome = project.gradle.getGradleUserHomeDir()
          systemProperty 'gradle.lib.dir', Path.of(project.class.location.toURI()).parent.toAbsolutePath().toString().replace('\\', '/')
          systemProperty 'gradle.worker.jar', Path.of("${gradleUserHome}/caches/${gradle.gradleVersion}/workerMain/gradle-worker.jar").toAbsolutePath().toString()
          systemProperty 'gradle.user.home', gradleUserHome.toPath().toAbsolutePath().toString()
        }

        doFirst {
          logger.debug("Will use test opts:\n" + testOptionsResolved.collect {k,v -> "${k}: ${v}"}.sort().join("\n"))
        }
      }
    }
  }
}

// Add a helper task to display resolved test property values with their defaults
// and descriptions.
allprojects {
  plugins.withType(JavaPlugin) {
    task testOpts() {
      group = 'Help (developer guides and hints)'
      description = "Display values of randomization settings for a given seed"

      doFirst {
        println "Test options for project ${project.path} and seed \"${rootSeed}\":"

        testOptions.sort { a, b -> a.propName.compareTo(b.propName) }.each { opt ->
          def defValue
          def computedValue = false
          if (opt.value instanceof Closure) {
            defValue = Objects.toString(opt.value(), null)
            computedValue = true
          } else {
            defValue = Objects.toString(opt.value, null)
          }

          def value = testOptionsResolved[opt.propName]
          println String.format(Locale.ROOT,
              "%s%-24s = %-8s # %s",
              (defValue != value ? "! " : computedValue ? "C " : "  "),
              opt.propName,
              value,
              (computedValue ? "(!= default: computed) " : (defValue != value ? "(!= default: ${defValue}) " : "")) + opt.description)
        }
      }
    }
  }
}
