package com.hcl.devops.test

import java.util.concurrent.TimeUnit

import org.apache.http.client.methods.CloseableHttpResponse
import org.apache.http.client.methods.HttpGet
import org.apache.http.client.methods.HttpPost
import org.apache.http.entity.StringEntity
import org.apache.http.util.EntityUtils

import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.google.gson.JsonArray
import com.google.gson.JsonElement
import com.google.gson.JsonObject
import com.google.gson.JsonParser
import com.hcl.devops.test.util.JobProperties
import com.hcl.devops.test.util.TestHubClient

import groovy.json.JsonSlurper

class OneTestUCDHelper {

   JobProperties jobProperties
   TestHubClient testHub
   String type
   String desktopProjectId

   OneTestUCDHelper(JobProperties jobProperties) {
      println '[Info]  Plugin version 5.0'
      this.jobProperties = jobProperties
      if (jobProperties.insecure) {
         testHub = new TestHubClient(jobProperties.serverUrl, jobProperties.offlineToken)
      } else {
         testHub = new TestHubClient(jobProperties.serverUrl, jobProperties.offlineToken, jobProperties.trustStorePath, jobProperties.trustStorePassword)
      }
   }

   void startJob() {
      String projId = getProjectIdByName(jobProperties.projName)
      String assetId = getAssetByName(projId)
      if (type == 'APISUITE' || type == 'APISTUB' || type == 'APITEST') {
         if (jobProperties.ritEnv) {
            validateEnvironment(jobProperties.ritEnv, projId)
         } else {
            println('[Error] Environment is mandatory attribute in order to run API based tests')
            System.exit(1)
         }
      } else {
         jobProperties.ritEnv = null
      }

      HttpPost startJobMethod = new HttpPost("${jobProperties.serverUrl}/rest/projects/${projId}/executions/")
      startJobMethod.addHeader('Accept', 'application/json')

       String methodBody
       if(jobProperties.ritEnv) { // Include environment in the request body
           methodBody = '{"testAsset": {' +
                   '"assetId": "' + assetId + '",' +
                   '"revision": "' + jobProperties.branchName + '"' +
                   '},' +
                   '"offlineToken": "' + testHub.refreshToken + '",' +
                   '"environment": "' + jobProperties.ritEnv  + '"}'
       } else {
           methodBody = '{"testAsset": {' +
                   '"assetId": "' + assetId + '",' +
                   '"revision": "' + jobProperties.branchName + '"' +
                   '},' +
                   '"offlineToken": "' + testHub.refreshToken + '"}'
       }

      JsonObject reqObject = new JsonParser().parse(methodBody).asJsonObject
      if (jobProperties.datasetsString) {
         String[] datasetList = jobProperties.datasetsString.split(';')
         JsonArray datasetArray = new JsonArray()
         for (String datasets : datasetList) {
            String[] datasetsArray = datasets.split(':')
            if (datasetsArray.length != 2) {
               throw new Exception('Please enter Dataset value in format -- SourceDataset:SwapDataset')
            } else if (datasetsArray[0] == null || datasetsArray[0].trim().empty) {
               throw new Exception('Source Dataset is not given for Swapdataset')
            } else if (datasetsArray[1] == null || datasetsArray[1].trim().empty) {
               throw new Exception('SwapDataset is not given for Source Dataset')
            }
            String srcId = getsrcDatasetId(projId, assetId, jobProperties.branchName, datasetsArray[0])
            String repId = getReplaceDatasetId(projId, srcId, jobProperties.branchName, datasetsArray)

            JsonObject sourceObj = new JsonObject()
            JsonObject assetObj = new JsonObject()
            JsonObject replaceObj = new JsonObject()

            assetObj.addProperty('assetId', srcId)
            sourceObj.add('source', assetObj)
            replaceObj.addProperty('datasetId', repId)
            sourceObj.add('replacement', replaceObj)

            datasetArray.add(sourceObj)
         }
         reqObject.add('dataSources', datasetArray)
      }

      if (jobProperties.variablesString) {
         JsonObject variableObject = new JsonObject()
         for (variable in jobProperties.variablesString.split(';')) {
            variableObject.addProperty(variable.tokenize('=')[0].trim(), variable.tokenize('=')[1])
         }
         reqObject.add('variables', variableObject)
      }

      if (jobProperties.resultPropsString) {
         JsonObject resPropObject = new JsonObject()
         for (resProp in jobProperties.resultPropsString.split(';')) {
            resPropObject.addProperty('result.property.' + resProp.tokenize('=')[0].trim(), resProp.tokenize('=')[1])
         }
         JsonObject advancedObject = new JsonObject()
         advancedObject.add('configuration', resPropObject)
         reqObject.add('advancedSettings', advancedObject)
      }

      if (jobProperties.tagsString) {
         Gson gson = new GsonBuilder().create()
         JsonElement jsonElement = gson.toJsonTree(jobProperties.tagsString?.split(',')*.trim().collect { it })
         reqObject.add('tags', jsonElement)
      }

      if (jobProperties.secretsCollectionName) {
         String secretId = getSecretId(projId, jobProperties.secretsCollectionName)
         reqObject.addProperty('secretsCollection', secretId)
      }

      startJobMethod.entity = new StringEntity(reqObject.toString())
      CloseableHttpResponse resp = testHub.execute(startJobMethod)
      int statusCode = resp.statusLine.statusCode
      if (statusCode != 201) {
         println "[Error] Request failed with status ${statusCode}. Exiting Failure."
         println('Response:\n' + resp.entity?.content?.getText('UTF-8'))
         System.exit(1)
      }
      Map parsedJson = new JsonSlurper().parseText(EntityUtils.toString(resp.entity))
      String execId = parsedJson.id
      String resultId = parsedJson.result.id
      String status = parsedJson.status
      String prevStatus = status
      println "Execution ID: ${execId} Results ID: ${resultId} \nExecution Status: ${status}"

      if (!(status == 'COMPLETE' || status == 'COMPLETE_WITH_ERROR' || status == 'STOPPED_BY_USER' ||
            status == 'STOPPED_AUTOMATICALLY' || status == 'INCOMPLETE' || status == 'CANCELED' || status == 'LAUNCH_FAILED')) {
         String statusURL = "${jobProperties.serverUrl}/rest/projects/${projId}/executions/${execId}"

         while (!(status == 'COMPLETE' || status == 'COMPLETE_WITH_ERROR' || status == 'STOPPED_BY_USER' ||
               status == 'STOPPED_AUTOMATICALLY' || status == 'INCOMPLETE' || status == 'CANCELED' || status == 'LAUNCH_FAILED')) {
            TimeUnit.SECONDS.sleep(10)
            HttpGet get = new HttpGet(statusURL)
            CloseableHttpResponse response = testHub.execute(get)
            String resEntity = EntityUtils.toString(response.entity)
            if (!resEntity.empty) {
               status = new JsonSlurper().parseText(resEntity).status
            }
            //The output step property is set to Success for APISTUB once it starts as the APISTUB keeps running until explicitly stopped.
            if (type == 'APISTUB' && status == 'RUNNING') {
               println "Execution Status: ${status}"
               jobProperties.setOutputProperty('Status', 'Success')
               break
            }
            if (status != null && prevStatus != status) {
               println "Execution Status: ${status}"
            }
            prevStatus = status
         }

         if (type != 'APISTUB') {
            getVerdict(projId, resultId, status)
         }
      } else {
         if (type != 'APISTUB') {
            getVerdict(projId, resultId, status)
         }
      }

      if (statusCode != 201) {
         println "[Error] Request failed with status ${statusCode}. Exiting Failure."
         println('Response:\n' + resp.entity?.content?.getText('UTF-8'))
         System.exit(1)
      }
   }

   // Find projectID from given project name using server side filtering.
   String getProjectIdByName(String projName) {
      String url = jobProperties.serverUrl + '/rest/projects?archived=false&member=true&name=' + URLEncoder.encode(projName, 'UTF-8')
      HttpGet get = new HttpGet(url)
      if (jobProperties.teamSpace) {
         String spaceId = getTeamSpaceIdByName(jobProperties.teamSpace)
         println '[Info]  Limiting project search to teamspaceID ' + spaceId
         get.addHeader('spaceId', spaceId)
      }
      println "[API]   getProjectIdByName: '${get}'"
      def parsedJson = new JsonSlurper().parseText(EntityUtils.toString(testHub.execute(get).entity))
      def parsedData = parsedJson.data
      String projId
      String nonMatches = ''
      if (parsedData && parsedData.size() == 1) {
         projId = parsedData[0].id
         println '[Info]  Matched single project \"' + parsedData[0].name + '\" based on filter of \"' + projName + '\"'
      } else {
         for (def project : parsedData) {
            if (projName == project.name) {
               projId = project.id
               println '[Info]  Matched project \"' + project.name + '\"'
            } else {
               nonMatches += '\"' + project.name + '\" (' + project.id + '), '
            }
         }
      }
      if (!projId) {
         if (nonMatches.length() == 0) {
            println '[Error] The project \"' + projName + '\" was not found or you do not have access. Please check the Project field in the task.'
         } else {
            println '[Error] The project \"' + projName + "\" was not distinct enough and didn't directly match one of " + nonMatches + ' please check the Project field in the task.'
         }
         System.exit(1)
      }
      return projId
   }

   // Find spaceID from given teamspace name/slug using server side filtering.
   String getTeamSpaceIdByName(String teamSpace) {
      String url = jobProperties.serverUrl + '/rest/spaces?search=' + URLEncoder.encode(teamSpace, 'UTF-8') + '&member=true'
      url = URLifyII(url)
      HttpGet get = new HttpGet(url)
      println "[API]   getTeamSpaceIdByName: '${get}'"
      def parsedJson = new JsonSlurper().parseText(EntityUtils.toString(testHub.execute(get).entity))
      String nonMatches = ''
      String spaceId
      boolean spaceFound = false
      if (parsedJson && parsedJson.size() == 1) {
         spaceId = parsedJson[0].id
         spaceFound = true
         println '[Info]  Matched single TeamSpace \"' + parsedJson[0].displayName + '\"'
      } else {
         for (def space : parsedJson) {
            if (teamSpace == space.displayName || teamSpace == space.slug) {
               spaceId = space.id
               if (spaceFound) {
                  println '[Error] Matched another TeamSpace \"' + space.displayName + '\" update your configuration to specify only one TeamSpace'
                  System.exit(1)
               } else {
                  spaceFound = true
                  println '[Info]  Matched TeamSpace \"' + space.displayName + '\" by name or slug'
               }
            } else {
               nonMatches += '\"' + space.displayName + '\" (' + space.slug + '), '
            }
         }
      }
      if (!spaceFound) {
         if (nonMatches.length() == 0) {
            println '[Error] The team space \"' + teamSpace + '\" was not found or you do not have access. Please check the Team Space field in the task.'
         } else {
            println '[Error] The team space \"' + teamSpace + "\" was not distinct enough and didn't directly match one of " + nonMatches + ' please check the Team Space field in the task.'
         }
         System.exit(1)
      }
      return spaceId
   }

   // Find repoID for a project with optional filtering of repo URI
   String getRepoIdByName(String repoName, String projId) {
      String url = jobProperties.serverUrl + '/rest/projects/' + projId + '/repositories/'
      if (repoName) {
         url += '?searchText=' + repoName
      }
      url = URLifyII(url)
      HttpGet get = new HttpGet(url)
      println "[API]   getRepoIdByName: '${get}'"
      def parsedJson = new JsonSlurper().parseText(EntityUtils.toString(testHub.execute(get).entity))
      def repoArray = parsedJson.content
      String nonMatches = ''
      String repoId
      if (repoArray && repoArray.size() == 1) {
         repoId = repoArray[0].id
         println '[Info]  Matched single Repository \"' + repoArray[0].uri + '\" based on filter of \"' + repoName + '\"'
         if (!jobProperties.branchName || jobProperties.branchName.length() == 0) {
            jobProperties.branchName = repoArray[0].defaultBranch
            println '[Info]  Branch Name not specified, defaulting to \"' + repoArray[0].defaultBranch + '\"'
         }
      } else if (repoName) {
         for (def repos : repoArray) {
            if (repos.uri  == repoName) {
               repoId = repos.id
               println '[Info]  Matched Repository \"' + repos.uri + '\" by name.'
               if (!jobProperties.branchName || jobProperties.branchName.length() == 0) {
                  jobProperties.branchName = repos.defaultBranch
                  println '[Info]  Branch Name not specified, defaulting to \"' + repos.defaultBranch + '\"'
               }
            } else {
               nonMatches += '\"' + repos.uri + '\" (' + repos.id + '), '
            }
         }
         if (!repoId) {
            if (nonMatches.length() == 0) {
               println '[Error] The Repository \"' + repoName + '\" was not found or you do not have access. Please check the Repository Link field in the task.'
            } else {
               println '[Error] The Repository \"' + repoName + "\" was not distinct enough and didn't directly match one of " + nonMatches + ' please check the Repository Link field in the task.'
            }
            System.exit(1)
         }
      }
      return repoId
   }

   // Find assetID for a named test asset with optional filtering based on repository
   String getAssetByName(String projId) {
      String url = "${jobProperties.serverUrl}/rest/projects/${projId}/assets/?name=" + URLEncoder.encode(jobProperties.filepath, 'UTF-8')
      String repoId
      if (jobProperties.repoName) {
         repoId =  getRepoIdByName(jobProperties.repoName, projId)
         url += '&repositoryId=' + repoId
      }
      if (jobProperties.branchName) {
         url += '&revision=' + URLEncoder.encode(jobProperties.branchName, 'UTF-8')
      }
      url = URLifyII(url)
      HttpGet get = new HttpGet(url)
      println "[API]   getAssetByName: '${get}'"
      def parsedJson = new JsonSlurper().parseText(EntityUtils.toString(testHub.execute(get).entity))
      def assetArray = parsedJson.content
      String assetId
      long nonMatches = 0
      if (assetArray && assetArray.size() == 1) {
         assetId = assetArray[0].id
         type = assetArray[0].external_type
         desktopProjectId = assetArray[0].desktop_project_id
         println "[Info]  Matched single test asset \"${assetArray[0].path}\" based on filter of \"${jobProperties.filepath}\""
      } else {
         for (def asset : assetArray) {
            if (asset.path == jobProperties.filepath && asset.name != null && (!repoId || asset.repository_id == repoId)) {
               assetId = asset.id
               type = asset.external_type
               desktopProjectId = asset.desktop_project_id
               println '[Info]  Matched test asset \"' + asset.path + '\"'
            } else {
               nonMatches++
            }
         }
      }
      if (!assetId) {
         if (nonMatches == 0) {
            println "[Error] The test asset \"${jobProperties.filepath}\" was not found or you do not have access. Please check the Asset Name/Path field in the task."
         } else {
            println "[Error] The test asset \"${jobProperties.filepath}\" was not distinct enough and matched ${nonMatches} assets, please update the Asset Name/Path field in the task."
         }
         System.exit(1)
      }
      return assetId
   }

   // Set the output status of the task based on the result printing more information if cancelled or failed.
   void getVerdict(String projId, String resultId, String status) {
      HttpGet get = new HttpGet("${jobProperties.serverUrl}/rest/projects/${projId}/results/${resultId}")
      get.addHeader('Content-Type', 'application/json')
      println "[API]   getVerdict: '${get}'"
      def parsedJson =  new JsonSlurper().parseText(EntityUtils.toString(testHub.execute(get).entity))
      def verdict = parsedJson.verdict
      println('\nTest Result: ' + verdict)

      if (verdict == null || verdict == 'FAIL' || verdict == 'ERROR') {
         jobProperties.setOutputProperty('Status', 'Failure')
      }
      else {
         jobProperties.setOutputProperty('Status', 'Success')
      }

      if (!(status == 'CANCELED' || status == 'LAUNCH_FAILED')) {
         def reportArray = parsedJson.reports
         if (reportArray.size() > 0) {
            println('\nReports information:')
            for (def jsonObj : reportArray) {
               String reportName = jsonObj.name
               String href = jsonObj.href
               println(reportName + ' : ' + new URI(jobProperties.serverUrl).resolve(href))
            }
         }
      }
   }

   String URLifyII(String text) {
      int startIndex = 0
      int endIndex = text.length() - 1
      StringBuilder urlifiedText = new StringBuilder()

      // Find first non-space character
      while (text.charAt(startIndex) == ' ' && startIndex < endIndex) {
         startIndex++
      }

      // Find last non-space character
      while (text.charAt(endIndex) == ' ' && endIndex >= startIndex) {
         endIndex--
      }

      // Repeat text, and replace spaces with %20
      for (int i = startIndex; i <= endIndex; i++) {
         if (text.charAt(i) != ' ') {
            urlifiedText.append(text.charAt(i))
         } else {
            urlifiedText.append('%20')
         }
      }
      return urlifiedText.toString()
   }

   // Validate that the named environment exists
   void validateEnvironment(String ritEnv, String projId) {
      String encodedBranchName = URLEncoder.encode(jobProperties.branchName, 'UTF-8')
      HttpGet get = new HttpGet("${jobProperties.serverUrl}/rest/projects/${projId}/assets/?assetTypes=environment&revision=${encodedBranchName}&desktopProjectId=${desktopProjectId}")
      def parsedJson = new JsonSlurper().parseText(EntityUtils.toString(testHub.execute(get).entity))
      def dataArray = parsedJson.content
      for (def data : dataArray) {
         if (ritEnv == data.name) {
            return
         }
      }
      println "The test environment ${ritEnv} is not valid for the test."
      System.exit(1)
   }

   // Get the ID of a named dataset used by a test.
   String getsrcDatasetId(String projId, String assetId, String branch, String srcDataSet) {
      String url = "${jobProperties.serverUrl}/rest/projects/${projId}/assets/${assetId}/${branch}/dependencies/?assetTypes=dataset"
      url = URLifyII(url)
      HttpGet get = new HttpGet(url)
      println "getsrcDatasetId: '${get}'"
      def parsedJson = new JsonSlurper().parseText(EntityUtils.toString(testHub.execute(get).entity))
      def dataArray = parsedJson.content
      String srcdataSetId = null
      for (def data : dataArray) {
         if (data.path == srcDataSet) {
            srcdataSetId = data.id
         }
      }
      if (!srcdataSetId) {
         println "Source Dataset \"${srcDataSet}\" not found in test asset"
         System.exit(1)
      }
      return srcdataSetId
   }

   // Get the ID of a named dataset replacement candidate of a test.
   String getReplaceDatasetId(String projId, String srcDatasetId, String branch, String[] repDataSet) {
      String url = "${jobProperties.serverUrl}/rest/projects/${projId}/datasets/?branch=${branch}&assetId=${srcDatasetId}&findSwaps=true"
      url = URLifyII(url)
      HttpGet get = new HttpGet(url)
      println "getReplaceDatasetId: '${get}'"
      def parsedJson = new JsonSlurper().parseText(EntityUtils.toString(testHub.execute(get).entity))
      def dataArray = parsedJson.data
      String repdataSetId = null
      for (def data : dataArray) {
         if (data.displayPath == repDataSet[1]) {
            repdataSetId = data.datasetId
         }
      }
      if (!repdataSetId) {
         println "No SwapDataset (${repDataSet[1]}) is configured for the Source Dataset('${repDataSet[0]})'"
         System.exit(1)
      }
      return repdataSetId
   }

   // Get the ID of a named secrets collection
   String getSecretId(String projId, String secretName) {
      String url = "${jobProperties.serverUrl}/rest/projects/${projId}/secrets/?type=ENVIRONMENT"
      url = URLifyII(url)
      HttpGet get = new HttpGet(url)
      println "getSecretId: '${get}'"
      def parsedJson = new JsonSlurper().parseText(EntityUtils.toString(testHub.execute(get).entity))
      def dataArray = parsedJson.data
      String secretId = null
      for (def data : dataArray) {
         if (data.name == secretName) {
            secretId = data.id
         }
      }
      if (!secretId) {
         println 'No Secret configured on the Server.'
      }
      return secretId
   }
}
