require 'net/ssh/buffer'
require 'net/ssh/packet'
require 'net/ssh/buffered_io'
require 'net/ssh/connection/channel'
require 'net/ssh/connection/constants'
require 'net/ssh/transport/constants'
require 'net/ssh/transport/packet_stream'

module Net
  module SSH
    module Test
      # A collection of modules used to extend/override the default behavior of
      # Net::SSH internals for ease of testing. As a consumer of Net::SSH, you'll
      # never need to use this directly--they're all used under the covers by
      # the Net::SSH::Test system.
      module Extensions
        # An extension to Net::SSH::BufferedIo (assumes that the underlying IO
        # is actually a StringIO). Facilitates unit testing.
        module BufferedIo
          # Returns +true+ if the position in the stream is less than the total
          # length of the stream.
          def select_for_read?
            pos < size
          end

          # Set this to +true+ if you want the IO to pretend to be available for writing
          attr_accessor :select_for_write

          # Set this to +true+ if you want the IO to pretend to be in an error state
          attr_accessor :select_for_error

          alias select_for_write? select_for_write
          alias select_for_error? select_for_error
        end

        # An extension to Net::SSH::Transport::PacketStream (assumes that the
        # underlying IO is actually a StringIO). Facilitates unit testing.
        module PacketStream
          include BufferedIo # make sure we get the extensions here, too

          def self.included(base) # :nodoc:
            base.send :alias_method, :real_available_for_read?, :available_for_read?
            base.send :alias_method, :available_for_read?, :test_available_for_read?

            base.send :alias_method, :real_enqueue_packet, :enqueue_packet
            base.send :alias_method, :enqueue_packet, :test_enqueue_packet

            base.send :alias_method, :real_poll_next_packet, :poll_next_packet
            base.send :alias_method, :poll_next_packet, :test_poll_next_packet
          end

          # Called when another packet should be inspected from the current
          # script. If the next packet is a remote packet, it pops it off the
          # script and shoves it onto this IO object, making it available to
          # be read.
          def idle!
            return false unless script.next(:first)

            if script.next(:first).remote?
              current_pos = pos
              self.string = "#{self.string}#{script.next}"
              self.pos = current_pos
            end

            return true
          end

          # The testing version of Net::SSH::Transport::PacketStream#available_for_read?.
          # Returns true if there is data pending to be read. Otherwise calls #idle!.
          def test_available_for_read?
            return true if select_for_read?

            idle!
            false
          end

          # The testing version of Net::SSH::Transport::PacketStream#enqueued_packet.
          # Simply calls Net::SSH::Test::Script#process on the packet.
          def test_enqueue_packet(payload)
            packet = Net::SSH::Buffer.new(payload.to_s)
            script.process(packet)
          end

          # The testing version of Net::SSH::Transport::PacketStream#poll_next_packet.
          # Reads the next available packet from the IO object and returns it.
          def test_poll_next_packet
            return nil if available <= 0

            packet = Net::SSH::Buffer.new(read_available(4))
            length = packet.read_long
            Net::SSH::Packet.new(read_available(length))
          end
        end

        # An extension to Net::SSH::Connection::Channel. Facilitates unit testing.
        module Channel
          def self.included(base) # :nodoc:
            base.send :alias_method, :send_data_for_real, :send_data
            base.send :alias_method, :send_data, :send_data_for_test
          end

          # The testing version of Net::SSH::Connection::Channel#send_data. Calls
          # the original implementation, and then immediately enqueues the data for
          # output so that scripted sends are properly interpreted as discrete
          # (rather than concatenated) data packets.
          def send_data_for_test(data)
            send_data_for_real(data)
            enqueue_pending_output
          end
        end

        # An extension to the built-in ::IO class. Simply redefines IO.select
        # so that it can be scripted in Net::SSH unit tests.
        module IO
          def self.included(base) # :nodoc:
            base.extend(ClassMethods)
          end

          @extension_enabled = false

          def self.with_test_extension(&block)
            orig_value = @extension_enabled
            @extension_enabled = true
            begin
              yield
            ensure
              @extension_enabled = orig_value
            end
          end

          def self.extension_enabled?
            @extension_enabled
          end

          module ClassMethods
            def self.extended(obj) # :nodoc:
              class << obj
                alias_method :select_for_real, :select
                alias_method :select, :select_for_test
              end
            end

            # The testing version of ::IO.select. Assumes that all readers,
            # writers, and errors arrays are either nil, or contain only objects
            # that mix in Net::SSH::Test::Extensions::BufferedIo.
            def select_for_test(readers = nil, writers = nil, errors = nil, wait = nil)
              return select_for_real(readers, writers, errors, wait) unless Net::SSH::Test::Extensions::IO.extension_enabled?

              ready_readers = Array(readers).select { |r| r.select_for_read? }
              ready_writers = Array(writers).select { |r| r.select_for_write? }
              ready_errors  = Array(errors).select  { |r| r.select_for_error? }

              return [ready_readers, ready_writers, ready_errors] if ready_readers.any? || ready_writers.any? || ready_errors.any?

              processed = 0
              Array(readers).each do |reader|
                processed += 1 if reader.idle!
              end

              raise "no readers were ready for reading, and none had any incoming packets" if processed == 0 && wait != 0

              [[], [], []]
            end
          end
        end
      end
    end
  end
end

Net::SSH::BufferedIo.send(:include, Net::SSH::Test::Extensions::BufferedIo)
Net::SSH::Transport::PacketStream.send(:include, Net::SSH::Test::Extensions::PacketStream)
Net::SSH::Connection::Channel.send(:include, Net::SSH::Test::Extensions::Channel)
IO.send(:include, Net::SSH::Test::Extensions::IO)
