RSpec.describe RubySMB::Dispatcher::Socket do
  class FakeSocket < StringIO
    def setsockopt(*args); end
  end

  let(:fake_tcp_socket) {
    FakeSocket.new('deadbeef')
  }

  subject(:smb_socket) { described_class.new(fake_tcp_socket) }
  let(:negotiate_response) { RubySMB::SMB2::Packet::NegotiateResponse.new }
  let(:nbss) { smb_socket.nbss(negotiate_response) }
  let(:response_packet) { nbss + negotiate_response.to_binary_s }

  # Don't try to actually select on our StringIO fake socket
  before(:each) do
    allow(IO).to receive(:select).and_return([])
  end

  it 'should attempt to set KEEPALIVE on the socket' do
    expect(fake_tcp_socket).to receive(:setsockopt).with(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, true)
    described_class.new(fake_tcp_socket)
  end

  it 'sets the default read_timeout value to READ_TIMEOUT' do
    expect(described_class.new(fake_tcp_socket).read_timeout).to eq(described_class::READ_TIMEOUT)
  end

  it 'accepts a read_timeout value as arguments' do
    read_timeout = 10
    expect(described_class.new(fake_tcp_socket, read_timeout: read_timeout).read_timeout).to eq(read_timeout)
  end

  describe '#connect' do
    it 'should support setting a custom socket object' do
      socket = described_class.connect('172.16.22.165', socket: fake_tcp_socket)
      expect(socket.tcp_socket).to eq(fake_tcp_socket)
    end

    it 'should default to setting up a TCPSocket' do
      host = '172.16.22.165'
      port = 445
      expect(TCPSocket).to receive(:new).with(host, port)
      described_class.connect(host)
    end
  end

  describe '#recv_packet' do
    let(:blank_socket)    { StringIO.new('') }
    let(:response_socket) { StringIO.new(response_packet) }
    let(:session_header)  { RubySMB::Nbss::SessionHeader.new }

    context 'when reading from the socket results in a nil value' do
      it 'should raise Error::CommunicationError' do
        smb_socket.tcp_socket = blank_socket
        expect { smb_socket.recv_packet }.to raise_error(::RubySMB::Error::CommunicationError)
      end
    end

    context 'when reading from the socket results in an empty value' do
      it 'should raise Error::NetBiosSessionService' do
        smb_socket.tcp_socket = blank_socket
        allow(blank_socket).to receive(:read).and_return('')
        expect { smb_socket.recv_packet }.to raise_error(::RubySMB::Error::NetBiosSessionService)
      end
    end

    context 'when a socket error occurs' do
      it 'raises a CommunicationError exception' do
        expect(fake_tcp_socket).to receive(:read).and_raise(Errno::ECONNRESET)
        expect { smb_socket.recv_packet }.to raise_error(RubySMB::Error::CommunicationError)
      end
    end

    context 'when the read_timeout expires' do
      it 'raises a CommunicationError exception' do
        allow(IO).to receive(:select).and_return(nil)
        expect { smb_socket.recv_packet }.to raise_error(RubySMB::Error::CommunicationError)
      end
    end

    context 'when reading an SMB Response packet' do
      before :example do
        smb_socket.tcp_socket = response_socket
      end

      it 'reads the nbss header using Nbss::SessionHeader structure' do
        expect(RubySMB::Nbss::SessionHeader).to receive(:read).with(nbss).and_return(session_header)
        smb_socket.recv_packet
      end

      it 'uses the default read_timeout with IO#select when it has not been specifically defined' do
        expect(IO).to receive(:select).with([response_socket], nil, nil, described_class::READ_TIMEOUT).and_return([]).twice
        smb_socket.recv_packet
      end

      it 'uses the defined read_timeout with IO#select' do
        timeout = 10
        smb_socket.read_timeout = timeout
        expect(IO).to receive(:select).with([response_socket], nil, nil, timeout).and_return([]).twice
        smb_socket.recv_packet
      end

      it 'reads the number of bytes specified in the nbss header' do
        stream_protocol_length = 10
        session_header.stream_protocol_length = stream_protocol_length
        allow(RubySMB::Nbss::SessionHeader).to receive(:read).and_return(session_header)
        expect(response_socket).to receive(:read).with(4).and_return(session_header)
        expect(response_socket).to receive(:read).with(stream_protocol_length).and_return('A' * stream_protocol_length)
        smb_socket.recv_packet
      end

      context 'when the socket does not return everything the first time' do
        it 'calls #read several times until everything is returned' do
          stream_protocol_length = 67
          returned_length = 10
          session_header.stream_protocol_length = stream_protocol_length
          allow(RubySMB::Nbss::SessionHeader).to receive(:read).and_return(session_header)

          expect(response_socket).to receive(:read).with(4).and_return(session_header)
          loop do
            expect(response_socket).to receive(:read).with(stream_protocol_length).and_return('A' * returned_length).once
            stream_protocol_length -= returned_length
            break if stream_protocol_length <= 0
          end
          smb_socket.recv_packet
        end
      end

      context 'when the full_response argument is true' do
        it 'returns the full packet including the nbss header' do
          expect(smb_socket.recv_packet(full_response: true)).to eq(response_packet)
        end

        context 'when the SMB packet is empty' do
          it 'returns the nbss header only' do
            session_header.stream_protocol_length = 0
            allow(RubySMB::Nbss::SessionHeader).to receive(:read).and_return(session_header)
            expect(smb_socket.recv_packet(full_response: true)).to eq(session_header.to_binary_s)
          end
        end
      end

      context 'when the full_response argument is false' do
        it 'returns the SMB packet without the nbss header' do
          expect(smb_socket.recv_packet(full_response: false)).to eq(negotiate_response.to_binary_s)
        end

        context 'when the SMB packet is empty' do
          it 'returns an empty string' do
            session_header.stream_protocol_length = 0
            allow(RubySMB::Nbss::SessionHeader).to receive(:read).and_return(session_header)
            expect(smb_socket.recv_packet(full_response: false)).to eq('')
          end
        end
      end
    end
  end

  describe '#send_packet' do
    context 'when nbss_header argument is true' do
      it 'calls #nbss to create the nbss header for the packet' do
        expect(smb_socket).to receive(:nbss).with(negotiate_response).and_return(nbss)
        smb_socket.send_packet(negotiate_response, nbss_header: true)
      end
    end

    context 'when nbss_header argument is false' do
      it 'does not call #nbss' do
        expect(smb_socket).to_not receive(:nbss)
        smb_socket.send_packet(negotiate_response, nbss_header: false)
      end
    end

    it 'writes the packet to the socket' do
      expect(fake_tcp_socket).to receive(:write).with(response_packet).and_call_original
      smb_socket.send_packet(negotiate_response)
    end

    it 'raises a CommunicationError if it encounters a socket error' do
      expect(fake_tcp_socket).to receive(:write).and_raise(Errno::ECONNRESET)
      expect { smb_socket.send_packet(negotiate_response) }.to raise_error(RubySMB::Error::CommunicationError)
    end
  end
end
