# frozen_string_literal: true

module TestQueue
  class Iterator
    include Enumerable

    attr_reader :sock

    def initialize(test_framework, sock, filter = nil, run_token:, early_failure_limit: nil)
      @test_framework = test_framework
      @done = false
      @suite_stats = []
      @sock = sock
      @filter = filter
      if @sock =~ /\A(.+):(\d+)\z/
        @tcp_address = $1
        @tcp_port = $2.to_i
      end
      @failures = 0
      @early_failure_limit = early_failure_limit
      @run_token = run_token
    end

    def each
      raise "already used this iterator. previous caller: #{@done}" if @done

      procline = $0

      loop do
        # If we've hit too many failures in one worker, assume the entire
        # test suite is broken, and notify master so the run
        # can be immediately halted.
        if @early_failure_limit && @failures >= @early_failure_limit
          connect_to_master('KABOOM')
          break
        else
          client = connect_to_master("POP #{Socket.gethostname} #{Process.pid}")
        end
        break if client.nil?

        # This false positive will be resolved by https://github.com/rubocop/rubocop/pull/11830.
        _r, _w, e = IO.select([client], nil, [client], nil)
        break unless e.empty?

        if (data = client.read(65536))
          client.close
          item = Marshal.load(data)
          break if item.nil? || item.empty?

          if item == 'WAIT'
            $0 = "#{procline} - Waiting for work"
            sleep 0.1
            next
          end
          suite_name, path = item
          suite = load_suite(suite_name, path)

          # Maybe we were told to load a suite that doesn't exist anymore.
          next unless suite

          $0 = "#{procline} - #{suite.respond_to?(:description) ? suite.description : suite}"
          start = Time.now
          if @filter
            @filter.call(suite) { yield suite }
          else
            yield suite
          end
          @suite_stats << TestQueue::Stats::Suite.new(suite_name, path, Time.now - start, Time.now)
          @failures += suite.failure_count if suite.respond_to? :failure_count
        else
          break
        end
      end
    rescue Errno::ENOENT, Errno::ECONNRESET, Errno::ECONNREFUSED
      # noop
    ensure
      $0 = procline
      @done = caller(1..1).first
      File.open("/tmp/test_queue_worker_#{$$}_suites", 'wb') do |f|
        Marshal.dump(@suite_stats, f)
      end
    end

    def connect_to_master(cmd)
      sock =
        if @tcp_address
          TCPSocket.new(@tcp_address, @tcp_port)
        else
          UNIXSocket.new(@sock)
        end
      sock.puts("TOKEN=#{@run_token}")
      sock.puts(cmd)
      sock
    rescue Errno::EPIPE
      nil
    end

    def empty?
      false
    end

    def load_suite(suite_name, path)
      @loaded_suites ||= {}
      suite = @loaded_suites[suite_name]
      return suite if suite

      @test_framework.suites_from_file(path).each do |name, suite_from_file|
        @loaded_suites[name] = suite_from_file
      end
      @loaded_suites[suite_name]
    end
  end
end
