require 'fastlane_core/test_parser'
require_relative 'simulator_launcher_base'

module Snapshot
  class CPUInspector
    def self.hwprefs_available?
      !!FastlaneCore::Helper.which('hwprefs')
    end

    def self.cpu_count
      @cpu_count ||=
        case RUBY_PLATFORM
        when /darwin9/
          `hwprefs cpu_count`.to_i
        when /darwin10/
          (hwprefs_available? ? `hwprefs thread_count` : `sysctl -n hw.physicalcpu_max`).to_i
        when /linux/
          UI.user_error!("We detected that you are running snapshot on Linux, but snapshot is only supported on macOS")
        when /freebsd/
          UI.user_error!("We detected that you are running snapshot on FreeBSD, but snapshot is only supported on macOS")
        else
          if RbConfig::CONFIG['host_os'] =~ /darwin/
            (hwprefs_available? ? `hwprefs thread_count` : `sysctl -n hw.physicalcpu_max`).to_i
          else
            UI.crash!("Cannot find the machine's processor count.")
          end
        end
    end
  end
  class SimulatorLauncher < SimulatorLauncherBase
    # With Xcode 9's ability to run tests on multiple concurrent simulators,
    # this method sets the maximum number of simulators to run simultaneously
    # to avoid overloading your machine.
    def default_number_of_simultaneous_simulators
      cpu_count = CPUInspector.cpu_count
      if cpu_count <= 2
        return cpu_count
      end

      return cpu_count - 1
    end

    def take_screenshots_simultaneously
      languages_finished = {}
      launcher_config.launch_args_set.each do |launch_args|
        launcher_config.languages.each_with_index do |language, language_index|
          locale = nil
          if language.kind_of?(Array)
            locale = language[1]
            language = language[0]
          end

          # Clear logs so subsequent xcodebuild executions don't append to old ones
          log_path = xcodebuild_log_path(language: language, locale: locale)
          File.delete(log_path) if File.exist?(log_path)

          # Break up the array of devices into chunks that can
          # be run simultaneously.
          if launcher_config.concurrent_simulators?
            all_devices = launcher_config.devices
            # We have to break up the concurrent simulators by device version too, otherwise there is an error (see #10969)
            by_simulator_version = all_devices.group_by { |d| FastlaneCore::DeviceManager.latest_simulator_version_for_device(d) }.values
            device_batches = by_simulator_version.flat_map { |a| a.each_slice(default_number_of_simultaneous_simulators).to_a }
          else
            # Put each device in it's own array to run tests one at a time
            device_batches = launcher_config.devices.map { |d| [d] }
          end

          device_batches.each do |devices|
            languages_finished[language] = launch_simultaneously(devices, language, locale, launch_args)
          end
        end
      end
      launcher_config.devices.each_with_object({}) do |device, results_hash|
        results_hash[device] = languages_finished
      end
    end

    def launch_simultaneously(devices, language, locale, launch_arguments)
      prepare_for_launch(devices, language, locale, launch_arguments)

      add_media(devices, :photo, launcher_config.add_photos) if launcher_config.add_photos
      add_media(devices, :video, launcher_config.add_videos) if launcher_config.add_videos

      command = TestCommandGenerator.generate(
        devices: devices,
        language: language,
        locale: locale,
        log_path: xcodebuild_log_path(language: language, locale: locale)
      )

      devices.each { |device_type| override_status_bar(device_type, launcher_config.override_status_bar_arguments) } if launcher_config.override_status_bar

      UI.important("Running snapshot on: #{devices.join(', ')}")

      execute(command: command, language: language, locale: locale, launch_args: launch_arguments, devices: devices)

      devices.each { |device_type| clear_status_bar(device_type) } if launcher_config.override_status_bar

      return copy_screenshots(language: language, locale: locale, launch_args: launch_arguments)
    end

    def execute(retries = 0, command: nil, language: nil, locale: nil, launch_args: nil, devices: nil)
      prefix_hash = [
        {
          prefix: "Running Tests: ",
          block: proc do |value|
            value.include?("Touching")
          end
        }
      ]
      error_proc = proc do |output, return_code|
        self.collected_errors.concat(failed_devices.map do |device, messages|
          "#{device}: #{messages.join(', ')}"
        end)

        cleanup_after_failure(devices, language, locale, launch_args, return_code)

        # no exception raised... that means we need to retry
        UI.error("Caught error... #{return_code}")

        self.current_number_of_retries_due_to_failing_simulator += 1
        if self.current_number_of_retries_due_to_failing_simulator < 20 && return_code != 65
          # If the return code is not 65, we should assume its a simulator failure and retry
          launch_simultaneously(devices, language, locale, launch_args)
        elsif retries < launcher_config.number_of_retries
          # If there are retries remaining, run the tests again
          retry_tests(retries, command, language, locale, launch_args, devices)
        else
          # It's important to raise an error, as we don't want to collect the screenshots
          UI.crash!("Too many errors... no more retries...") if launcher_config.stop_after_first_error
        end
      end
      FastlaneCore::CommandExecutor.execute(command: command,
                                          print_all: true,
                                      print_command: true,
                                             prefix: prefix_hash,
                                            loading: "Loading...",
                                              error: error_proc)
    end

    def cleanup_after_failure(devices, language, locale, launch_args, return_code)
      copy_screenshots(language: language, locale: locale, launch_args: launch_args)

      UI.important("Tests failed while running on: #{devices.join(', ')}")
      UI.important("For more detail about the test failures, check the logs here:")
      UI.important(xcodebuild_log_path(language: language, locale: locale))
      UI.important(" ")
      UI.important("You can also find the test result data here:")
      UI.important(test_results_path)
      UI.important(" ")
      UI.important("You can find the incomplete screenshots here:")
      UI.important(SCREENSHOTS_DIR)
      UI.important(launcher_config.output_directory)
    end

    def retry_tests(retries, command, language, locale, launch_args, devices)
      UI.important("Retrying on devices: #{devices.join(', ')}")
      UI.important("Number of retries remaining: #{launcher_config.number_of_retries - retries - 1}")

      # Make sure all needed directories exist for next retry
      # `copy_screenshots` in `cleanup_after_failure` can delete some needed directories
      # https://github.com/fastlane/fastlane/issues/10786
      prepare_directories_for_launch(language: language, locale: locale, launch_arguments: launch_args)

      # Clear errors so a successful retry isn't reported as an over failure
      self.collected_errors = []

      execute(retries + 1, command: command, language: language, locale: locale, launch_args: launch_args, devices: devices)
    end

    def copy_screenshots(language: nil, locale: nil, launch_args: nil)
      raw_output = File.read(xcodebuild_log_path(language: language, locale: locale))
      dir_name = locale || language
      return Collector.fetch_screenshots(raw_output, dir_name, '', launch_args.first)
    end

    def test_results_path
      derived_data_path = TestCommandGenerator.derived_data_path
      return File.join(derived_data_path, 'Logs/Test')
    end

    # This method returns a hash of { device name => [failure messages] }
    # {
    #   'iPhone 7': [], # this empty array indicates success
    #   'iPhone 7 Plus': ["No tests were executed"],
    #   'iPad Air': ["Launch session expired", "Array out of bounds"]
    # }
    def failed_devices
      test_summaries = Dir["#{test_results_path}/*_TestSummaries.plist"]
      test_summaries.each_with_object({}) do |plist, hash|
        summary = FastlaneCore::TestParser.new(plist)
        name = summary.data.first[:run_destination_name]
        if summary.data.first[:number_of_tests] == 0
          hash[name] = ["No tests were executed"]
        else
          tests = Array(summary.data.first[:tests])
          hash[name] = tests.flat_map { |test| Array(test[:failures]).map { |failure| failure[:failure_message] } }
        end
      end
    end

    def xcodebuild_log_path(language: nil, locale: nil)
      name_components = [Snapshot.project.app_name, Snapshot.config[:scheme]]

      if Snapshot.config[:namespace_log_files]
        name_components << launcher_config.devices.join('-') if launcher_config.devices.count >= 1
        name_components << language if language
        name_components << locale if locale
      end

      file_name = "#{name_components.join('-')}.log"

      containing = File.expand_path(Snapshot.config[:buildlog_path])
      FileUtils.mkdir_p(containing)

      return File.join(containing, file_name)
    end
  end
end
