/*
 * Copyright ConsenSys AG.
 *
 * 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.
 *
 * SPDX-License-Identifier: Apache-2.0
 */

configurations {
  // we need this because referenceTestImplementation defaults to 'canBeResolved=false'.
  tarConfig.extendsFrom referenceTestImplementation
  tarConfig {
    canBeResolved = true
    canBeConsumed = false
  }
}

def blockchainReferenceTests = tasks.register("blockchainReferenceTests")  {
  final referenceTestsPath = 'src/reference-test/external-resources/BlockchainTests'
  final generatedTestsPath = "$buildDir/generated/sources/reference-test/$name/java"
  inputs.files fileTree(referenceTestsPath),
    fileTree(generatedTestsPath)
  outputs.files generatedTestsPath
  generateTestFiles(
    fileTree(referenceTestsPath),
    file("src/reference-test/templates/BlockchainReferenceTest.java.template"),
    "BlockchainTests",
    "$generatedTestsPath/org/hyperledger/besu/ethereum/vm/blockchain",
    "BlockchainReferenceTest",
    ("BlockchainTests/InvalidBlocks/bcExpectSection") // exclude test for test filling tool
    )
}

def eipBlockchainReferenceTests = tasks.register("eipBlockchainReferenceTests")  {
  final referenceTestsPath = 'src/reference-test/external-resources/EIPTests/BlockchainTests'
  final generatedTestsPath = "$buildDir/generated/sources/reference-test/$name/java"
  inputs.files fileTree(referenceTestsPath),
    fileTree(generatedTestsPath)
  outputs.files generatedTestsPath
  generateTestFiles(
    fileTree(referenceTestsPath),
    file("src/reference-test/templates/BlockchainReferenceTest.java.template"),
    "EIPTests${File.separatorChar}BlockchainTests",
    "$generatedTestsPath/org/hyperledger/besu/ethereum/vm/eip",
    "EIPBlockchainReferenceTest",
    )
}

def eipStateReferenceTests = tasks.register("eipStateReferenceTests")  {
  final referenceTestsPath = 'src/reference-test/external-resources/EIPTests/StateTests'
  final generatedTestsPath = "$buildDir/generated/sources/reference-test/$name/java"
  inputs.files fileTree(referenceTestsPath),
    fileTree(generatedTestsPath)
  outputs.files generatedTestsPath
  generateTestFiles(
    fileTree(referenceTestsPath),
    file("src/reference-test/templates/GeneralStateReferenceTest.java.template"),
    "EIPTests${File.separatorChar}StateTests",
    "$generatedTestsPath/org/hyperledger/besu/ethereum/vm/eip",
    "EIPStateReferenceTest",
    )
}

def executionSpecTests = tasks.register("executionSpecTests") {
  final referenceTestsPath = "$buildDir/execution-spec-tests/"
  final generatedTestsPath = "$buildDir/generated/sources/reference-test/$name/java"
  def tarPath = configurations.tarConfig.files.find{ it.name.startsWith('execution-spec-tests')}

  copy {
    from tarTree(tarPath)
    into "$referenceTestsPath"
  }

  inputs.files fileTree(referenceTestsPath), fileTree(generatedTestsPath)
  outputs.files generatedTestsPath
  generateTestFiles(
    fileTree(referenceTestsPath + "/fixtures"),
    file("src/reference-test/templates/BlockchainReferenceTest.java.template"),
    "fixtures",
    "$generatedTestsPath/org/hyperledger/besu/ethereum/vm/executionspec",
    "ExecutionSpecTest",
    ("fixtures/example/example") // exclude test for test filling tool
    )
}

def generalstateReferenceTests = tasks.register("generalstateReferenceTests")  {
  final referenceTestsPath = "src/reference-test/external-resources/GeneralStateTests"
  final generatedTestsPath = "$buildDir/generated/sources/reference-test/$name/java"
  inputs.files fileTree(referenceTestsPath),
    fileTree(generatedTestsPath)
  outputs.files generatedTestsPath
  generateTestFiles(
    fileTree(referenceTestsPath),
    file("src/reference-test/templates/GeneralStateReferenceTest.java.template"),
    "GeneralStateTests",
    "$generatedTestsPath/org/hyperledger/besu/ethereum/vm/generalstate",
    "GeneralStateReferenceTest"
    )
}

def generalstateRegressionReferenceTests = tasks.register("generalstateRegressionReferenceTests") {
  final referenceTestsPath = "src/reference-test/resources/regressions/generalstate"
  final generatedTestsPath = "$buildDir/generated/sources/reference-test/$name/java"
  inputs.files fileTree(referenceTestsPath),
    fileTree(generatedTestsPath)
  outputs.files generatedTestsPath
  generateTestFiles(
    fileTree(referenceTestsPath),
    file("src/reference-test/templates/GeneralStateReferenceTest.java.template"),
    "regressions",
    "$generatedTestsPath/org/hyperledger/besu/ethereum/vm/generalstate",
    "GeneralStateRegressionReferenceTest"
    )
}

sourceSets {
  referenceTest {
    java {
      compileClasspath += main.output
      runtimeClasspath += main.output
      srcDirs "src/reference-test/java",
        blockchainReferenceTests,
        eipBlockchainReferenceTests,
        eipStateReferenceTests,
        executionSpecTests,
        generalstateReferenceTests,
        generalstateRegressionReferenceTests
    }
    resources {
      srcDirs 'src/reference-test/resources',
        'src/reference-test/external-resources',
        'src/reference-test/templates',
        'build/execution-spec-tests/'
    }
  }
}

dependencies {
  implementation project(':config')
  implementation project(':crypto:algorithms')
  implementation project(':datatypes')
  implementation project(':ethereum:core')
  implementation project(':metrics:core')
  implementation project(':util')
  implementation project(path: ':ethereum:core', configuration: 'testSupportArtifacts')
  implementation project(':ethereum:rlp')
  implementation project(':evm')
  implementation project(':services:kvstore')

  implementation 'io.tmio:tuweni-rlp'
  implementation 'com.fasterxml.jackson.core:jackson-databind'
  implementation 'com.google.guava:guava'

  referenceTestImplementation project(path: ':config')
  referenceTestImplementation project(path: ':datatypes')
  referenceTestImplementation project(path: ':ethereum:core')
  referenceTestImplementation project(path: ':ethereum:core', configuration: 'testSupportArtifacts')
  referenceTestImplementation project(path: ':ethereum:rlp')
  referenceTestImplementation project(path: ':ethereum:rlp', configuration: 'testSupportArtifacts')
  referenceTestImplementation project(path: ':ethereum:trie')
  referenceTestImplementation project(path: ':evm')
  referenceTestImplementation project(path: ':plugin-api')
  referenceTestImplementation project(path: ':testutil')
  referenceTestImplementation project(path: ':util')
  // the following will be resolved via custom ivy repository declared in root build.gradle
  referenceTestImplementation 'ethereum:execution-spec-tests:0.2.5:fixtures@tar.gz'
  referenceTestImplementation 'com.fasterxml.jackson.core:jackson-databind'
  referenceTestImplementation 'com.google.guava:guava'
  referenceTestImplementation 'io.tmio:tuweni-bytes'
  referenceTestImplementation 'io.tmio:tuweni-units'
  referenceTestImplementation 'org.assertj:assertj-core'
  referenceTestImplementation 'org.junit.jupiter:junit-jupiter-api'
  referenceTestImplementation 'org.junit.jupiter:junit-jupiter-params'

  referenceTestRuntimeOnly 'org.junit.jupiter:junit-jupiter'
}

tasks.register('referenceTests', Test) {
  useJUnitPlatform()
  doFirst {
    if (!file("src/reference-test/external-resources/README.md").exists()) {
      throw new GradleException("ethereum/referencetest/src/reference-test/external-resources/README.md missing: please clone submodules (git submodule update --init --recursive)")
    }
  }
  description = 'Runs ETH reference tests.'
  testClassesDirs = sourceSets.referenceTest.output.classesDirs
  classpath = sourceSets.referenceTest.runtimeClasspath
}

tasks.register('validateReferenceTestSubmodule') {
  description = "Checks that the reference tests submodule is not accidentally changed"
  doLast {
    def result = new ByteArrayOutputStream()
    def expectedHash = '853b1e03b1078d370614002851ba1ee9803d9fcf '
    def submodulePath = java.nio.file.Path.of("${rootProject.projectDir}", "ethereum/referencetests/src/reference-test/external-resources").toAbsolutePath()
    try {
      exec {
        commandLine 'git', 'submodule', 'status', submodulePath
        standardOutput = result
        errorOutput = result
      }
    } catch (Exception ignore) {
      // Ignore it.  We want to fail in a friendly fashion if they don't have git installed.
      // The CI servers have git and that is the only critical place for this failure
      expectedHash = ''
    }

    if (!result.toString().contains(expectedHash)) {
      throw new GradleException("""For the Ethereum Reference Tests the git commit did not match what was expected.
  
If this is a deliberate change where you are updating the reference tests 
then update "expectedHash" in `ethereum/referencetests/build.gradle` as the 
commit hash for this task.
Expected hash   :  ${expectedHash}
Full git output : ${result}

If this is accidental you can correct the reference test versions with the 
following commands:
    pushd ${submodulePath}
    git fetch
    git checkout ${expectedHash}
    cd ..
    git add ${submodulePath}
    popd""")
    }
  }
}
processResources.dependsOn('validateReferenceTestSubmodule')

def generateTestFiles(FileTree jsonPath, File templateFile, String pathstrip, String destination, String namePrefix, String ... excludedPath) {
  mkdir(destination)
  def referenceTestTemplate = templateFile.text

  // This is how many json files to include in each test file
  def fileSets = jsonPath.getFiles().collate(5)

  fileSets.eachWithIndex { fileSet, idx ->
    def paths = []
    fileSet.each { testJsonFile ->
      def parentFile = testJsonFile.getParentFile()
      def parentPathFile = parentFile.getPath().substring(parentFile.getPath().indexOf(pathstrip))
      if (!testJsonFile.getName().toString().startsWith(".") && !excludedPath.contains(parentPathFile)) {
        def pathFile = testJsonFile.getPath()
        paths << pathFile.substring(pathFile.indexOf(pathstrip))
      }
    }

    def testFile = file(destination + "/" + namePrefix + "_" + idx + ".java")


    def allPaths = '"' + paths.join('", "') + '"'

    def testFileContents = referenceTestTemplate
      .replaceAll("%%TESTS_FILE%%", allPaths)
      .replaceAll("%%TESTS_NAME%%", namePrefix + "_" + idx)
    testFile.newWriter().withWriter { w -> w << testFileContents }
  }
}
