require 'nokogiri'

module Danger

  # Parse a Kover or Jacoco report to enforce code coverage on CI. Results are passed out as a table in markdown.
  #
  # Shroud depends on having a Kover or Jacoco coverage report generated for your project.
  #
  #
  # @example Running Shroud with default values for Kover
  #
  #          # Report coverage of modified files, fail if either total project coverage
  #          # or any modified file's coverage is under 90%
  #          shroud.reportKover 'Project Name', 'path/to/kover/report.xml'
  #
  # @example Running Shroud with custom coverage thresholds for Kover
  #
  #          # Report coverage of modified files, fail if total project coverage is under 80%,
  #          # or if any modified file's coverage is under 95%
  #          shroud.reportKover 'Project Name', 'path/to/kover/report.xml', 80, 95
  #
  # @example Warn on builds instead of fail for Kover
  #
  #          # Report coverage of modified files the same as the above example, except the
  #          # builds will only warn instead of fail if below thresholds
  #          shroud.reportKover 'Project Name', 'path/to/kover/report.xml', 80, 95, false
  #
  # @example Running Shroud with default values for Jacoco
  #
  #          # Report coverage of modified files, fail if either total project coverage
  #          # or any modified file's coverage is under 90%
  #          shroud.reportJacoco 'Project Name', 'path/to/jacoco/report.xml'
  #
  # @example Running Shroud with custom coverage thresholds for Jacoco
  #
  #          # Report coverage of modified files, fail if total project coverage is under 80%,
  #          # or if any modified file's coverage is under 95%
  #          shroud.reportJacoco 'Project Name', 'path/to/jacoco/report.xml', 80, 95
  #
  # @example Warn on builds instead of fail for Jacoco
  #
  #          # Report coverage of modified files the same as the above example, except the
  #          # builds will only warn instead of fail if below thresholds
  #          shroud.reportJacoco 'Project Name', 'path/to/jacoco/report.xml', 80, 95, false
  #          
  # @tags android, kover, jacoco, coverage
  #
  class DangerShroud < Plugin

    # <b>DEPRECATED:</b> Please use <tt>reportJacoco</tt> or <tt>reportKover</tt> instead.
    #
    # Report coverage on diffed files, as well as overall coverage.
    #
    # @param   [String] file
    #          file path to a Jacoco xml coverage report.
    #
    # @param   [Integer] totalProjectThreshold
    #          defines the required percentage of total project coverage for a passing build.
    #          default 90.
    #
    # @param   [Integer] modifiedFileThreshold
    #          defines the required percentage of files modified in a PR for a passing build.
    #          default 90.
    #
    # @param   [Boolean] failIfUnderThreshold
    #          if true, will fail builds that are under the provided thresholds. if false, will only warn.
    #          default true.
    #
    # @return  [void]
    def report(file, totalProjectThreshold = 90, modifiedFileThreshold = 90, failIfUnderThreshold = true)
      warn "[DEPRECATION] `report` is deprecated.  Please use `reportJacoco` or `reportKover` instead."
      reportJacoco('Project', file, totalProjectThreshold = 90, modifiedFileThreshold = 90, failIfUnderThreshold = true)
    end

    # Report coverage on diffed files, as well as overall coverage.
    #
    # @param   [String] moduleName
    #          the display name of the project or module
    #
    # @param   [String] file
    #          file path to a Jacoco xml coverage report.
    #
    # @param   [Integer] totalProjectThreshold
    #          defines the required percentage of total project coverage for a passing build.
    #          default 90.
    #
    # @param   [Integer] modifiedFileThreshold
    #          defines the required percentage of files modified in a PR for a passing build.
    #          default 90.
    #
    # @param   [Boolean] failIfUnderThreshold
    #          if true, will fail builds that are under the provided thresholds. if false, will only warn.
    #          default true.
    #
    # @return  [void]
    def reportJacoco(moduleName, file, totalProjectThreshold = 90, modifiedFileThreshold = 90, failIfUnderThreshold = true)
      internalReport('Jacoco', moduleName, file, totalProjectThreshold, modifiedFileThreshold, failIfUnderThreshold)
    end

    # Report coverage on diffed files, as well as overall coverage.
    #
    # @param   [String] moduleName
    #          the display name of the project or module
    #
    # @param   [String] file
    #          file path to a Kover xml coverage report.
    #
    # @param   [Integer] totalProjectThreshold
    #          defines the required percentage of total project coverage for a passing build.
    #          default 90.
    #
    # @param   [Integer] modifiedFileThreshold
    #          defines the required percentage of files modified in a PR for a passing build.
    #          default 90.
    #
    # @param   [Boolean] failIfUnderThreshold
    #          if true, will fail builds that are under the provided thresholds. if false, will only warn.
    #          default true.
    #
    # @return  [void]
    def reportKover(moduleName, file, totalProjectThreshold = 90, modifiedFileThreshold = 90, failIfUnderThreshold = true)
      internalReport('Kover', moduleName, file, totalProjectThreshold, modifiedFileThreshold, failIfUnderThreshold)
    end

    private def internalReport(reportType, moduleName, file, totalProjectThreshold, modifiedFileThreshold, failIfUnderThreshold)
      raise "Please specify file name." if file.empty?
      raise "No #{reportType} xml report found at #{file}" unless File.exist? file
      rawXml = File.read(file)
      parsedXml = Nokogiri::XML.parse(rawXml)
      totalInstructionCoverage = parsedXml.xpath("/report/counter[@type='INSTRUCTION']")
      missed = totalInstructionCoverage.attr("missed").value.to_i
      covered = totalInstructionCoverage.attr("covered").value.to_i
      total = missed + covered
      coveragePercent = (covered / total.to_f) * 100

      # get array of files names touched by this PR (modified + added)
      touchedFileNames = @dangerfile.git.modified_files.map { |file| File.basename(file) }
      touchedFileNames += @dangerfile.git.added_files.map { |file| File.basename(file) }

      # used to later report files that were modified but not included in the report
      fileNamesNotInReport = []

      # hash for keeping track of coverage per filename: {filename => coverage percent}
      touchedFilesHash = {}

      touchedFileNames.each do |touchedFileName|
        xmlForFileName = parsedXml.xpath("//class[@sourcefilename='#{touchedFileName}']/counter[@type='INSTRUCTION']")

        if (xmlForFileName.length > 0)
          missed = 0
          covered = 0
          xmlForFileName.each do |classCountXml|
            missed += classCountXml.attr("missed").to_i
            covered += classCountXml.attr("covered").to_i
          end
          touchedFilesHash[touchedFileName] = (covered.to_f / (missed + covered)) * 100
        else
          fileNamesNotInReport << touchedFileName
        end
      end

      puts "Here are unreported files"
      puts fileNamesNotInReport.to_s
      puts "Here is the touched files coverage hash"
      puts touchedFilesHash

      output = "## 🧛 #{moduleName} Code Coverage: **`#{'%.2f' % coveragePercent}%`**\n"

      output << "### Coverage of Modified Files:\n"
      output << "File | Coverage\n"
      output << ":-----|:-----:\n"

      # go through each file:
      touchedFilesHash.sort.each do |fileName, coveragePercent|
        output << "`#{fileName}` | **`#{'%.2f' % coveragePercent}%`**\n"

        # warn or fail if under specified file threshold:
        if (coveragePercent < modifiedFileThreshold)
          warningMessage = "Uh oh! #{fileName} is under #{modifiedFileThreshold}% coverage!"
          if (failIfUnderThreshold)
            fail warningMessage
          else 
            warn warningMessage
          end
        end
      end

      output << "### Modified Files Not Found In Coverage Report:\n"
      fileNamesNotInReport.sort.each do |unreportedFileName| 
        output << "#{unreportedFileName}\n"
      end

      output << '> Codebase cunningly covered by count [Shroud 🧛](https://github.com/livefront/livefront-shroud-android/)'
      markdown output

      # warn or fail if total coverage is under specified threshold
      if (coveragePercent < totalProjectThreshold)
        totalCoverageWarning = "Uh oh! Your project is under #{totalProjectThreshold}% coverage!"
        if (failIfUnderThreshold) 
          fail totalCoverageWarning
        else 
          warn totalCoverageWarning
        end
      end
    end
  end
end