# frozen_string_literal: true

module KnapsackPro
  module Runners
    module Queue
      class RSpecRunner < BaseRunner
        def self.run(args, stream_error = $stderr, stream_out = $stdout)
          require 'rspec/core'
          require_relative '../../extensions/rspec_extension'
          require_relative '../../formatters/time_tracker'
          require_relative '../../formatters/time_tracker_fetcher'

          KnapsackPro::Extensions::RSpecExtension.setup!

          ENV['KNAPSACK_PRO_TEST_SUITE_TOKEN'] = KnapsackPro::Config::Env.test_suite_token_rspec

          rspec_pure = KnapsackPro::Pure::Queue::RSpecPure.new

          queue_runner = new(KnapsackPro::Adapters::RSpecAdapter, rspec_pure, args, stream_error, stream_out)
          queue_runner.run
        end

        def initialize(adapter_class, rspec_pure, args, stream_error, stream_out)
          ENV['KNAPSACK_PRO_RSPEC_OPTIONS'] = args.to_s
          super(adapter_class)
          @adapter_class = adapter_class
          @rspec_pure = rspec_pure
          args_array = (args || '').split
          has_format_option = @adapter_class.has_format_option?(args_array)
          has_require_rails_helper_option = @adapter_class.has_require_rails_helper_option?(args_array)
          rails_helper_exists = @adapter_class.rails_helper_exists?(test_dir)
          @cli_args = rspec_pure.prepare_cli_args(args, has_format_option, has_require_rails_helper_option, rails_helper_exists, test_dir)
          @stream_error = stream_error
          @stream_out = stream_out
          @node_test_file_paths = []
          @rspec_runner = nil # RSpec::Core::Runner is lazy initialized
          @queue = KnapsackPro::Queue.new
        end

        # Based on:
        # https://github.com/rspec/rspec-core/blob/f8c8880dabd8f0544a6f91d8d4c857c1bd8df903/lib/rspec/core/runner.rb#L85
        #
        # @return [Fixnum] exit status code.
        #   0 if all specs passed,
        #   or the configured failure exit code (1 by default) if specs failed.
        def run
          pre_run_setup

          if @rspec_runner.knapsack__wants_to_quit?
            exit_code = @rspec_runner.knapsack__exit_early
            Kernel.exit(exit_code)
          end

          begin
            exit_code = @rspec_runner.knapsack__run_specs(self)
          rescue KnapsackPro::Runners::Queue::BaseRunner::TerminationError
            exit_code = @rspec_pure.error_exit_code(@rspec_runner.knapsack__error_exit_code)
            Kernel.exit(exit_code)
          rescue KnapsackPro::QueueAllocator::FallbackModeError
            exit_code = KnapsackPro::Config::Env.fallback_mode_error_exit_code
            Kernel.exit(exit_code)
          rescue Exception => exception
            KnapsackPro.logger.error("An unexpected exception happened. RSpec cannot handle it. The exception: #{exception.inspect}")
            KnapsackPro.logger.error("Exception message: #{exception.message}")
            KnapsackPro.logger.error("Exception backtrace: #{exception.backtrace&.join("\n")}")

            message = @rspec_pure.exit_summary(unexecuted_test_files)
            KnapsackPro.logger.warn(message) if message

            exit_code = @rspec_pure.error_exit_code(@rspec_runner.knapsack__error_exit_code)
            Kernel.exit(exit_code)
          end

          post_run_tasks(exit_code)
        end

        def with_batch
          can_initialize_queue = true

          loop do
            handle_signal!
            test_file_paths = pull_tests_from_queue(can_initialize_queue: can_initialize_queue)
            can_initialize_queue = false

            break if test_file_paths.empty?

            subset_queue_id = KnapsackPro::Config::EnvGenerator.set_subset_queue_id
            ENV['KNAPSACK_PRO_SUBSET_QUEUE_ID'] = subset_queue_id

            @queue.add_batch_for(test_file_paths)

            KnapsackPro::Hooks::Queue.call_before_subset_queue(@queue)

            yield test_file_paths, @queue

            KnapsackPro::Hooks::Queue.call_after_subset_queue(@queue)

            if @rspec_runner.knapsack__wants_to_quit?
              KnapsackPro.logger.warn('RSpec wants to quit.')
              set_terminate_process
            end
            if @rspec_runner.knapsack__rspec_is_quitting?
              KnapsackPro.logger.warn('RSpec is quitting.')
              set_terminate_process
            end

            log_rspec_batch_command(test_file_paths)
          end
        end

        def handle_signal!
          self.class.handle_signal!
        end

        def log_fail_fast_limit_met
          KnapsackPro.logger.warn('Test execution has been canceled because the RSpec --fail-fast option is enabled. It will cause other CI nodes to run tests longer because they need to consume more tests from the Knapsack Pro Queue API.')
        end

        private

        def post_trap_signals
          RSpec.world.wants_to_quit = true

          log_current_batch_rspec_command
        end

        def log_current_tests(threads)
          threads.each do |thread|
            next unless thread.backtrace

            spec_file_lines = thread.backtrace.select { |line| line.include?('_spec.rb') }
            next if spec_file_lines.empty?

            puts
            if thread == Thread.main
              puts "Running specs in the main thread:"
            else
              puts "Non-main thread inspect: #{thread.inspect}"
              puts "Running specs in non-main thread:"
            end
            puts spec_file_lines.join("\n")
            puts
          end
        end

        def pre_run_setup
          ENV['KNAPSACK_PRO_QUEUE_MODE_ENABLED'] = 'true'
          ENV['KNAPSACK_PRO_QUEUE_ID'] = KnapsackPro::Config::EnvGenerator.set_queue_id

          KnapsackPro::Config::Env.set_test_runner_adapter(@adapter_class)

          ENV['SPEC_OPTS'] = @rspec_pure.add_knapsack_pro_formatters_to(ENV['SPEC_OPTS'])

          rspec_configuration_options = ::RSpec::Core::ConfigurationOptions.new(@cli_args)
          @rspec_runner = ::RSpec::Core::Runner.new(rspec_configuration_options)
          @rspec_runner.knapsack__setup(@stream_error, @stream_out)

          ensure_no_deprecated_run_all_when_everything_filtered_option!
        end

        def post_run_tasks(exit_code)
          @adapter_class.verify_bind_method_called

          log_rspec_queue_command

          time_tracker = KnapsackPro::Formatters::TimeTrackerFetcher.call
          KnapsackPro::Report.save_node_queue_to_api(time_tracker&.queue)

          Kernel.exit(exit_code)
        end

        def ensure_no_deprecated_run_all_when_everything_filtered_option!
          return unless @rspec_runner.knapsack__deprecated_run_all_when_everything_filtered_enabled?

          error_message = "The run_all_when_everything_filtered option is deprecated. See: #{KnapsackPro::Urls::RSPEC__DEPRECATED_RUN_ALL_WHEN_EVERYTHING_FILTERED}"
          KnapsackPro.logger.error(error_message)
          raise error_message
        end

        def pull_tests_from_queue(can_initialize_queue: false)
          test_file_paths = test_file_paths(
            can_initialize_queue: can_initialize_queue,
            executed_test_files: @node_test_file_paths
          )
          @node_test_file_paths += test_file_paths

          time_tracker = KnapsackPro::Formatters::TimeTrackerFetcher.call
          time_tracker.scheduled_paths = @node_test_file_paths

          test_file_paths
        end

        def log_current_batch_rspec_command
          test_file_paths = @queue.current_batch&.test_file_paths
          return unless test_file_paths

          puts
          puts '=' * 80

          order_option = @adapter_class.order_option(@cli_args)
          printable_args = @rspec_pure.args_with_seed_option_added_when_viable(order_option, @rspec_runner.knapsack__seed, @cli_args)
          messages = @rspec_pure.rspec_command(printable_args, test_file_paths, :batch_finished)
          messages.each do |message|
            puts message
          end
        end

        def log_rspec_batch_command(test_file_paths)
          order_option = @adapter_class.order_option(@cli_args)
          printable_args = @rspec_pure.args_with_seed_option_added_when_viable(order_option, @rspec_runner.knapsack__seed, @cli_args)
          messages = @rspec_pure.rspec_command(printable_args, test_file_paths, :batch_finished)
          log_info_messages(messages)
        end

        def log_rspec_queue_command
          order_option = @adapter_class.order_option(@cli_args)
          printable_args = @rspec_pure.args_with_seed_option_added_when_viable(order_option, @rspec_runner.knapsack__seed, @cli_args)
          messages = @rspec_pure.rspec_command(printable_args, @node_test_file_paths, :queue_finished)
          log_info_messages(messages)
        end

        def log_info_messages(messages)
          messages.each do |message|
            KnapsackPro.logger.info(message)
          end
        end

        def unexecuted_test_files
          KnapsackPro::Formatters::TimeTrackerFetcher.unexecuted_test_files
        end
      end
    end
  end
end
