require_relative 'xcpretty_reporter_options_generator'

module Scan
  # Responsible for building the fully working xcodebuild command
  class TestCommandGenerator
    def generate
      parts = prefix
      parts << Scan.config[:xcodebuild_command]
      parts += options
      parts += actions
      parts += suffix
      parts += pipe

      parts
    end

    def prefix
      prefixes = ["set -o pipefail &&"]

      package_path = Scan.config[:package_path]
      prefixes << "cd #{package_path} &&" if package_path.to_s != ""

      prefixes
    end

    # Path to the project or workspace as parameter
    # This will also include the scheme (if given)
    # @return [Array] The array with all the components to join
    def project_path_array
      unless Scan.config[:package_path].nil?
        params = []
        params << "-scheme #{Scan.config[:scheme].shellescape}" if Scan.config[:scheme]
        params << "-workspace #{Scan.config[:workspace].shellescape}" if Scan.config[:workspace]
        return params
      end

      proj = Scan.project.xcodebuild_parameters
      return proj if proj.count > 0
      UI.user_error!("No project/workspace found")
    end

    def options # rubocop:disable Metrics/PerceivedComplexity
      config = Scan.config

      options = []
      options += project_path_array unless config[:xctestrun]
      options << "-sdk '#{config[:sdk]}'" if config[:sdk]
      options << destination if destination # generated in `detect_values`
      options << "-toolchain '#{config[:toolchain]}'" if config[:toolchain]
      if config[:derived_data_path] && !options.include?("-derivedDataPath #{config[:derived_data_path].shellescape}")
        options << "-derivedDataPath #{config[:derived_data_path].shellescape}"
      end
      if config[:use_system_scm] && !options.include?("-scmProvider system")
        options << "-scmProvider system"
      end
      if config[:result_bundle_path]
        options << "-resultBundlePath #{config[:result_bundle_path].shellescape}"
        Scan.cache[:result_bundle_path] = config[:result_bundle_path]
      elsif config[:result_bundle]
        options << "-resultBundlePath #{result_bundle_path(true).shellescape}"
      end
      if FastlaneCore::Helper.xcode_at_least?(10)
        options << "-parallel-testing-enabled #{config[:parallel_testing] ? 'YES' : 'NO'}" unless config[:parallel_testing].nil?
        options << "-parallel-testing-worker-count #{config[:concurrent_workers]}" if config[:concurrent_workers]
        options << "-maximum-concurrent-test-simulator-destinations #{config[:max_concurrent_simulators]}" if config[:max_concurrent_simulators]
        options << "-disable-concurrent-testing" if config[:disable_concurrent_testing]
      end
      options << "-enableCodeCoverage #{config[:code_coverage] ? 'YES' : 'NO'}" unless config[:code_coverage].nil?
      options << "-enableAddressSanitizer #{config[:address_sanitizer] ? 'YES' : 'NO'}" unless config[:address_sanitizer].nil?
      options << "-enableThreadSanitizer #{config[:thread_sanitizer] ? 'YES' : 'NO'}" unless config[:thread_sanitizer].nil?
      if FastlaneCore::Helper.xcode_at_least?(11)
        if config[:cloned_source_packages_path] && !options.include?("-clonedSourcePackagesDirPath #{config[:cloned_source_packages_path].shellescape}")
          options << "-clonedSourcePackagesDirPath #{config[:cloned_source_packages_path].shellescape}"
        end

        if config[:package_cache_path] && !options.include?("-packageCachePath #{config[:package_cache_path].shellescape}")
          options << "-packageCachePath #{config[:package_cache_path].shellescape}"
        end

        options << "-testPlan '#{config[:testplan]}'" if config[:testplan]

        # detect_values will ensure that these values are present as Arrays if
        # they are present at all
        options += config[:only_test_configurations].map { |name| "-only-test-configuration '#{name}'" } if config[:only_test_configurations]
        options += config[:skip_test_configurations].map { |name| "-skip-test-configuration '#{name}'" } if config[:skip_test_configurations]
      end
      options << "-xctestrun '#{config[:xctestrun]}'" if config[:xctestrun]
      options << config[:xcargs] if config[:xcargs]

      # Number of retries does not equal xcodebuild's -test-iterations number
      # It needs include 1 iteration by default
      number_of_retries = config[:number_of_retries] + 1
      if number_of_retries > 1 && FastlaneCore::Helper.xcode_at_least?(13)
        options << "-retry-tests-on-failure"
        options << "-test-iterations #{number_of_retries}"
      end

      # detect_values will ensure that these values are present as Arrays if
      # they are present at all
      options += config[:only_testing].map { |test_id| "-only-testing:#{test_id.shellescape}" } if config[:only_testing]
      options += config[:skip_testing].map { |test_id| "-skip-testing:#{test_id.shellescape}" } if config[:skip_testing]

      options
    end

    def actions
      config = Scan.config

      actions = []
      actions << :clean if config[:clean]

      if config[:build_for_testing]
        actions << "build-for-testing"
      elsif config[:test_without_building] || config[:xctestrun]
        actions << "test-without-building"
      else
        actions << :build unless config[:skip_build]
        actions << :test
      end

      actions
    end

    def suffix
      suffix = []
      suffix
    end

    def pipe
      pipe = ["| tee '#{xcodebuild_log_path}'"]

      # disable_xcpretty is now deprecated and directs to use output_style of raw
      if Scan.config[:disable_xcpretty] || Scan.config[:output_style] == 'raw'
        return pipe
      end

      formatter = Scan.config[:xcodebuild_formatter].chomp
      options = legacy_xcpretty_options

      if formatter == ''
        UI.verbose("Not using an xcodebuild formatter")
      elsif !options.empty?
        UI.important("Detected legacy xcpretty being used, so formatting with xcpretty")
        UI.important("Option(s) used: #{options.join(', ')}")
        pipe << pipe_xcpretty
      elsif formatter == 'xcpretty'
        pipe << pipe_xcpretty
      elsif formatter == 'xcbeautify'
        pipe << pipe_xcbeautify
      else
        pipe << "| #{formatter}"
      end

      return pipe
    end

    def pipe_xcbeautify
      formatter = ['| xcbeautify']

      if FastlaneCore::Helper.colors_disabled?
        formatter << '--disable-colored-output'
      end

      return formatter.join(' ')
    end

    def legacy_xcpretty_options
      options = []

      options << "formatter" if Scan.config[:formatter]
      options << "xcpretty_formatter" if Scan.config[:xcpretty_formatter]
      options << "output_style" if Scan.config[:output_style]
      options << "output_types" if (Scan.config[:output_types] || "").include?("json-compilation-database")
      options << "custom_report_file_name" if Scan.config[:custom_report_file_name]

      return options
    end

    def pipe_xcpretty
      formatter = []
      if (custom_formatter = Scan.config[:xcpretty_formatter] || Scan.config[:formatter])
        if custom_formatter.end_with?(".rb")
          formatter << "-f '#{custom_formatter}'"
        else
          formatter << "-f `#{custom_formatter}`"
        end
      elsif FastlaneCore::Env.truthy?("TRAVIS")
        formatter << "-f `xcpretty-travis-formatter`"
        UI.success("Automatically switched to Travis formatter")
      end

      if Helper.colors_disabled?
        formatter << "--no-color"
      end

      if Scan.config[:output_style] == 'basic'
        formatter << "--no-utf"
      end

      if Scan.config[:output_style] == 'rspec'
        formatter << "--test"
      end

      @reporter_options_generator = XCPrettyReporterOptionsGenerator.new(Scan.config[:open_report],
                                                                         Scan.config[:output_types],
                                                                         Scan.config[:output_files] || Scan.config[:custom_report_file_name],
                                                                         Scan.config[:output_directory],
                                                                         Scan.config[:use_clang_report_name],
                                                                         Scan.config[:xcpretty_args])
      reporter_options = @reporter_options_generator.generate_reporter_options
      reporter_xcpretty_args = @reporter_options_generator.generate_xcpretty_args_options
      return "| xcpretty #{formatter.join(' ')} #{reporter_options.join(' ')} #{reporter_xcpretty_args}"
    end

    # Store the raw file
    def xcodebuild_log_path
      parts = []
      if Scan.config[:app_name]
        parts << Scan.config[:app_name]
      elsif Scan.project
        parts << Scan.project.app_name
      end
      parts << Scan.config[:scheme] if Scan.config[:scheme]

      file_name = "#{parts.join('-')}.log"
      containing = File.expand_path(Scan.config[:buildlog_path])
      FileUtils.mkdir_p(containing)

      return File.join(containing, file_name)
    end

    # Generate destination parameters
    def destination
      unless Scan.cache[:destination]
        Scan.cache[:destination] = [*Scan.config[:destination]].map { |dst| "-destination '#{dst}'" }.join(' ')
      end
      Scan.cache[:destination]
    end

    # The path to set the Derived Data to
    def build_path
      unless Scan.cache[:build_path]
        day = Time.now.strftime("%F") # e.g. 2015-08-07

        Scan.cache[:build_path] = File.expand_path("~/Library/Developer/Xcode/Archives/#{day}/")
        FileUtils.mkdir_p(Scan.cache[:build_path])
      end
      Scan.cache[:build_path]
    end

    # The path to the result bundle
    def result_bundle_path(use_output_directory)
      root_dir = use_output_directory ? Scan.config[:output_directory] : Dir.mktmpdir

      retry_count = Scan.cache[:retry_attempt] || 0
      attempt = retry_count > 0 ? "-#{retry_count}" : ""
      ext = FastlaneCore::Helper.xcode_version.to_i >= 11 ? '.xcresult' : '.test_result'
      path = File.join([root_dir, Scan.config[:scheme]].compact) + attempt + ext

      Scan.cache[:result_bundle_path] = path

      # The result bundle path will be in the package path directory if specified
      delete_path = path
      delete_path = File.join(Scan.config[:package_path], path) if Scan.config[:package_path].to_s != ""
      FileUtils.remove_dir(delete_path) if File.directory?(delete_path)

      return path
    end
  end
end
