require 'fastlane_core'
require 'pty'
require 'open3'
require 'tmpdir'
require 'fileutils'
require 'terminal-table'
require 'xcov-core'
require 'pathname'
require 'json'
require 'xcresult'

module Xcov
  class Manager

    def initialize(options)
      # Set command options
      Xcov.config = options

      # Set project options
      if !Xcov.config[:is_swift_package]
        FastlaneCore::Project.detect_projects(options)
        Xcov.project = FastlaneCore::Project.new(options)
      end

      # Set ignored files handler
      Xcov.ignore_handler = IgnoreHandler.new

      # Print summary
      FastlaneCore::PrintTable.print_values(config: options, hide_keys: [:slack_url, :coveralls_repo_token], title: "Summary for xcov #{Xcov::VERSION}")
    end

    def run
      # Run xcov
      json_report = parse_xccoverage
      report = generate_xcov_report(json_report)
      validate_report(report)
      submit_to_coveralls(report)
      tmp_dir = File.join(Xcov.config[:output_directory], 'tmp')
      FileUtils.rm_rf(tmp_dir) if File.directory?(tmp_dir)

      json_report
    end

    def parse_xccoverage
      xccoverage_files = []

      # xcresults to parse and export after collecting
      xcresults_to_parse_and_export = []

      # Find .xccoverage file
      # If no xccov direct path, use the old derived data path method
      if xccov_file_direct_paths.empty?
        extension = Xcov.config[:legacy_support] ? "xccoverage" : "xccovreport"
        
        test_logs_path = derived_data_path + "Logs/Test/"
        UI.important("Derived content from #{Dir["#{test_logs_path}/*"]}")
        
        xccoverage_files = Dir["#{test_logs_path}*.#{extension}", "#{test_logs_path}*.xcresult/*/action.#{extension}"]
        if xccoverage_files.empty?
          xcresult_paths = Dir["#{test_logs_path}*.xcresult"]
          xcresult_paths.each do |xcresult_path|
            xcresults_to_parse_and_export << xcresult_path
          end
        end

        unless test_logs_path.directory?
          ErrorHandler.handle_error("XccoverageFileNotFound")
        end
      else
        # Iterate over direct paths and find .xcresult files
        # that need to be processed before getting coverage
        xccov_file_direct_paths.each do |path|
          if File.extname(path) == '.xcresult'
            xcresults_to_parse_and_export << path
          else
            xccoverage_files << path
          end
        end
      end

      # Iterates over xcresults
      # Exports .xccovarchives
      # Exports .xccovreports and collects the paths
      # Merge .xccovreports if multiple exists and return merged report
      unless xcresults_to_parse_and_export.empty?
        xccoverage_files = process_xcresults!(xcresults_to_parse_and_export)
      end

      # Errors if no coverage files were found
      if xccoverage_files.empty?
        ErrorHandler.handle_error("XccoverageFileNotFound")
      end

      # Convert .xccoverage file to json
      ide_foundation_path = Xcov.config[:legacy_support] ? nil : Xcov.config[:ideFoundationPath]
      xccoverage_files = xccoverage_files.sort_by {|filename| File.mtime(filename)}.reverse
      json_report = Xcov::Core::Parser.parse(xccoverage_files.first, Xcov.config[:output_directory], ide_foundation_path)
      ErrorHandler.handle_error("UnableToParseXccoverageFile") if json_report.nil?

      json_report
    end

    private

    def generate_xcov_report(json_report)
      # Create output path
      output_path = Xcov.config[:output_directory]
      FileUtils.mkdir_p(output_path)

      # Convert report to xcov model objects
      report = Report.map(json_report)

      # Raise exception in case of failure
      ErrorHandler.handle_error("UnableToMapJsonToXcovModel") if report.nil?

      if Xcov.config[:html_report] then
        resources_path = File.join(output_path, "resources")
        FileUtils.mkdir_p(resources_path)

        # Copy images to output resources folder
        Dir[File.join(File.dirname(__FILE__), "../../assets/images/*")].each do |path|
          FileUtils.cp_r(path, resources_path)
        end

        # Copy stylesheets to output resources folder
        Dir[File.join(File.dirname(__FILE__), "../../assets/stylesheets/*")].each do |path|
          FileUtils.cp_r(path, resources_path)
        end

        # Copy javascripts to output resources folder
        Dir[File.join(File.dirname(__FILE__), "../../assets/javascripts/*")].each do |path|
          FileUtils.cp_r(path, resources_path)
        end

        # Create HTML report
        File.open(File.join(output_path, "index.html"), "wb") do |file|
          file.puts report.html_value
        end
      end

      # Create Markdown report
      if Xcov.config[:markdown_report] then
        File.open(File.join(output_path, "report.md"), "wb") do |file|
          file.puts report.markdown_value
        end
      end

      # Create JSON report
      if Xcov.config[:json_report] then
        File.open(File.join(output_path, "report.json"), "wb") do |file|
          file.puts report.json_value.to_json
        end
      end

      # Post result
      SlackPoster.new.run(report)

      # Print output
      table_rows = []
      report.targets.each do |target|
        table_rows << [target.name, target.displayable_coverage]
      end

      if report.targets.count > 0 
        table_rows << ["Average Coverage", "#{"%.2f%%" % (report.coverage*100)}"]
      end

      puts Terminal::Table.new({
        title: "xcov Coverage Report".green,
        rows: table_rows
      })
      puts ""

      report
    end

    def validate_report(report)
      # Raise exception if overall coverage is under threshold
      minimumPercentage = Xcov.config[:minimum_coverage_percentage] / 100
      if minimumPercentage > report.coverage
        error_message = "Actual Code Coverage (#{"%.2f%%" % (report.coverage*100)}) below threshold of #{"%.2f%%" % (minimumPercentage*100)}"
        ErrorHandler.handle_error_with_custom_message("CoverageUnderThreshold", error_message)
      end
    end

    def submit_to_coveralls(report)
      if Xcov.config[:disable_coveralls]
        return
      end
      if !Xcov.config[:coveralls_repo_token].nil? || !(Xcov.config[:coveralls_service_name].nil? && Xcov.config[:coveralls_service_job_id].nil?)
        CoverallsHandler.submit(report)
      end
    end

    # Auxiliar methods
    def derived_data_path
      # If DerivedData path was supplied, return
      return Pathname.new(Xcov.config[:derived_data_path]) unless Xcov.config[:derived_data_path].nil?

      # Otherwise check project file
      product_builds_path = Pathname.new(Xcov.project.default_build_settings(key: "SYMROOT"))
      return product_builds_path.parent.parent
    end

    def xccov_file_direct_paths
      # If xccov_file_direct_path was supplied, return
      if Xcov.config[:xccov_file_direct_path].nil?
          return []
      end

      paths = Xcov.config[:xccov_file_direct_path]
      return paths.map { |path| Pathname.new(path).to_s }
    end

    def process_xcresults!(xcresult_paths)
      output_path = Xcov.config[:output_directory]
      FileUtils.mkdir_p(output_path)
      
      result_path = ""
      index = 0
      
      xcresult_paths.flat_map do |xcresult_path|
        begin
          parser = XCResult::Parser.new(path: xcresult_path)
          
          # Exporting to same directory as xcresult
          tmp_archive_paths = parser.export_xccovarchives(destination: output_path)
          tmp_report_paths = parser.export_xccovreports(destination: output_path)

          # Rename each file with global index
          tmp_report_paths.each_with_index do |item, i|
            File.rename(tmp_archive_paths[i], "#{output_path}/xccovarchive-#{index + i}.xccovarchive")
            File.rename(item, "#{output_path}/xccovreport-#{index + i}.xccovreport")
            index += 1
          end
        rescue
          UI.error("Error occured while exporting xccovreport from xcresult '#{xcresult_path}'")
          UI.error("Make sure you have both Xcode 11 selected and pointing to the correct xcresult file")
          UI.crash!("Failed to export xccovreport from xcresult'")
        end
      end
      
      # Grab paths from the directory instead of parser
      report_paths = Dir["#{output_path}/*.xccovreport"]
      archive_paths = Dir["#{output_path}/*.xccovarchive"]
          
      # Merge coverage reports
      if report_paths.length > 1 then 
        # Creating array of paths for merging
        paths = ""
        for i in 0..report_paths.length
          paths += " #{report_paths[i]} #{archive_paths[i]}"
        end
            
        UI.important("Merging multiple coverage reports with #{paths}") 
        if system ( "xcrun xccov merge --outReport #{output_path}/out.xccovreport --outArchive #{output_path}/out.xccovarchive #{paths}" ) then
          result_path = "#{output_path}/out.xccovreport"
        else
          UI.error("Error occured during merging multiple coverage reports")
        end
      end

      if result_path == "" then
        # Informating user of export paths
        archive_paths.each do |path|
          UI.important("Copying .xccovarchive to #{path}") 
        end
        report_paths.each do |path|
          UI.important("Copying .xccovreport to #{path}") 
        end
            
        # Return array of report_paths if coverage reports were not merged
        return report_paths
      else
        # Return merged xccovreport
        return [result_path]
      end
    end
  end
end
