require 'spec_helper'

RSpec.describe HTTParty::Response do
  before do
    @last_modified = Date.new(2010, 1, 15).to_s
    @content_length = '1024'
    @request_object = HTTParty::Request.new Net::HTTP::Get, '/'
    @response_object = Net::HTTPOK.new('1.1', 200, 'OK')
    allow(@response_object).to receive_messages(body: "{foo:'bar'}")
    @response_object['last-modified'] = @last_modified
    @response_object['content-length'] = @content_length
    @parsed_response = lambda { {"foo" => "bar"} }
    @response = HTTParty::Response.new(@request_object, @response_object, @parsed_response)
  end

  describe ".underscore" do
    it "works with one capitalized word" do
      expect(HTTParty::Response.underscore("Accepted")).to eq("accepted")
    end

    it "works with titlecase" do
      expect(HTTParty::Response.underscore("BadGateway")).to eq("bad_gateway")
    end

    it "works with all caps" do
      expect(HTTParty::Response.underscore("OK")).to eq("ok")
    end
  end

  describe "initialization" do
    it "should set the Net::HTTP Response" do
      expect(@response.response).to eq(@response_object)
    end

    it "should set body" do
      expect(@response.body).to eq(@response_object.body)
    end

    it "should set code" do
      expect(@response.code).to eq(@response_object.code)
    end

    it "should set code as an Integer" do
      expect(@response.code).to be_a(Integer)
    end

    it "should set http_version" do
      unparseable_body = lambda { raise "Unparseable" }
      unparseable_response = HTTParty::Response.new(@request_object, @response_object, unparseable_body)
      expect(unparseable_response.http_version).to eq(@response_object.http_version)
    end

    context 'test raise_on requests' do
      let(:raise_on) { [404] }
      let(:request) { HTTParty::Request.new(Net::HTTP::Get, '/', raise_on: raise_on) }
      let(:body)     { 'Not Found' }
      let(:response) { Net::HTTPNotFound.new('1.1', 404, body) }

      subject { described_class.new(request, response, @parsed_response) }

      before do
        allow(response).to receive(:body).and_return(body)
      end

      context 'when raise_on is a number' do
        let(:raise_on) { [404] }

        context "and response's status code is in range" do
          it 'throws exception' do
            expect{ subject }.to raise_error(HTTParty::ResponseError, "Code 404 - #{body}")
          end
        end

        context "and response's status code is not in range" do
          let(:response) { Net::HTTPNotFound.new('1.1', 200, body) }

          it 'does not throw exception' do
            expect{ subject }.not_to raise_error
          end
        end
      end

      context 'when raise_on is a regexpr' do
        let(:raise_on) { ['4[0-9]*'] }

        context "and response's status code is in range" do
          it 'throws exception' do
            expect{ subject }.to raise_error(HTTParty::ResponseError, "Code 404 - #{body}")
          end
        end

        context "and response's status code is not in range" do
          let(:response) { Net::HTTPNotFound.new('1.1', 200, body) }

          it 'does not throw exception' do
            expect{ subject }.not_to raise_error
          end
        end
      end
    end
  end

  it 'does raise an error about itself when using #method' do
    expect {
      HTTParty::Response.new(@request_object, @response_object, @parsed_response).method(:qux)
    }.to raise_error(NameError, /HTTParty\:\:Response/)
  end

  it 'does raise an error about itself when invoking a method that does not exist' do
    expect {
      HTTParty::Response.new(@request_object, @response_object, @parsed_response).qux
    }.to raise_error(NoMethodError, /HTTParty\:\:Response/)
  end

  it "returns response headers" do
    response = HTTParty::Response.new(@request_object, @response_object, @parsed_response)
    expect(response.headers).to eq({'last-modified' => [@last_modified], 'content-length' => [@content_length]})
  end

  it "should send missing methods to delegate" do
    response = HTTParty::Response.new(@request_object, @response_object, @parsed_response)
    expect(response['foo']).to eq('bar')
  end

  it "responds to request" do
    response = HTTParty::Response.new(@request_object, @response_object, @parsed_response)
    expect(response.respond_to?(:request)).to be_truthy
  end

  it "responds to response" do
    response = HTTParty::Response.new(@request_object, @response_object, @parsed_response)
    expect(response.respond_to?(:response)).to be_truthy
  end

  it "responds to body" do
    response = HTTParty::Response.new(@request_object, @response_object, @parsed_response)
    expect(response.respond_to?(:body)).to be_truthy
  end

  it "responds to headers" do
    response = HTTParty::Response.new(@request_object, @response_object, @parsed_response)
    expect(response.respond_to?(:headers)).to be_truthy
  end

  it "responds to parsed_response" do
    response = HTTParty::Response.new(@request_object, @response_object, @parsed_response)
    expect(response.respond_to?(:parsed_response)).to be_truthy
  end

  it "responds to predicates" do
    response = HTTParty::Response.new(@request_object, @response_object, @parsed_response)
    expect(response.respond_to?(:success?)).to be_truthy
  end

  it "responds to anything parsed_response responds to" do
    response = HTTParty::Response.new(@request_object, @response_object, @parsed_response)
    expect(response.respond_to?(:[])).to be_truthy
  end

  context 'response is array' do
    let(:response_value) { [{'foo' => 'bar'}, {'foo' => 'baz'}] }
    let(:response) { HTTParty::Response.new(@request_object, @response_object, lambda { response_value }) }
    it "should be able to iterate" do
      expect(response.size).to eq(2)
      expect {
        response.each { |item| }
      }.to_not raise_error
    end

    it 'should respond to array methods' do
      expect(response).to respond_to(:bsearch, :compact, :cycle, :delete, :each, :flatten, :flatten!, :compact, :join)
    end

    it 'should equal the string response object body' do
      expect(response.to_s).to eq(@response_object.body.to_s)
    end

    it 'should display the same as an array' do
      a = StringIO.new
      b = StringIO.new
      response_value.display(b)
      response.display(a)

      expect(a.string).to eq(b.string)
    end
  end

  it "allows headers to be accessed by mixed-case names in hash notation" do
    response = HTTParty::Response.new(@request_object, @response_object, @parsed_response)
    expect(response.headers['Content-LENGTH']).to eq(@content_length)
  end

  it "returns a comma-delimited value when multiple values exist" do
    @response_object.add_field 'set-cookie', 'csrf_id=12345; path=/'
    @response_object.add_field 'set-cookie', '_github_ses=A123CdE; path=/'
    response = HTTParty::Response.new(@request_object, @response_object, @parsed_response)
    expect(response.headers['set-cookie']).to eq("csrf_id=12345; path=/, _github_ses=A123CdE; path=/")
  end

  # Backwards-compatibility - previously, #headers returned a Hash
  it "responds to hash methods" do
    response = HTTParty::Response.new(@request_object, @response_object, @parsed_response)
    hash_methods = {}.methods - response.headers.methods
    hash_methods.each do |method_name|
      expect(response.headers.respond_to?(method_name)).to be_truthy
    end
  end

  describe "#is_a?" do
    subject { HTTParty::Response.new(@request_object, @response_object, @parsed_response) }

    it { is_expected.to respond_to(:is_a?).with(1).arguments }
    it { expect(subject.is_a?(HTTParty::Response)).to be_truthy }
    it { expect(subject.is_a?(Object)).to be_truthy }
  end

  describe "#kind_of?" do
    subject { HTTParty::Response.new(@request_object, @response_object, @parsed_response) }

    it { is_expected.to respond_to(:kind_of?).with(1).arguments }
    it { expect(subject.kind_of?(HTTParty::Response)).to be_truthy }
    it { expect(subject.kind_of?(Object)).to be_truthy }
  end

  describe "semantic methods for response codes" do
    def response_mock(klass)
      response = klass.new('', '', '')
      allow(response).to receive(:body)
      response
    end

    context "major codes" do
      it "is information" do
        net_response = response_mock(Net::HTTPInformation)
        response = HTTParty::Response.new(@request_object, net_response, '')
        expect(response.information?).to be_truthy
      end

      it "is success" do
        net_response = response_mock(Net::HTTPSuccess)
        response = HTTParty::Response.new(@request_object, net_response, '')
        expect(response.success?).to be_truthy
      end

      it "is redirection" do
        net_response = response_mock(Net::HTTPRedirection)
        response = HTTParty::Response.new(@request_object, net_response, '')
        expect(response.redirection?).to be_truthy
      end

      it "is client error" do
        net_response = response_mock(Net::HTTPClientError)
        response = HTTParty::Response.new(@request_object, net_response, '')
        expect(response.client_error?).to be_truthy
      end

      it "is server error" do
        net_response = response_mock(Net::HTTPServerError)
        response = HTTParty::Response.new(@request_object, net_response, '')
        expect(response.server_error?).to be_truthy
      end
    end

    context "for specific codes" do
      SPECIFIC_CODES = {
        accepted?:                        Net::HTTPAccepted,
        bad_gateway?:                     Net::HTTPBadGateway,
        bad_request?:                     Net::HTTPBadRequest,
        conflict?:                        Net::HTTPConflict,
        continue?:                        Net::HTTPContinue,
        created?:                         Net::HTTPCreated,
        expectation_failed?:              Net::HTTPExpectationFailed,
        forbidden?:                       Net::HTTPForbidden,
        found?:                           Net::HTTPFound,
        gateway_time_out?:                Net::HTTPGatewayTimeOut,
        gone?:                            Net::HTTPGone,
        internal_server_error?:           Net::HTTPInternalServerError,
        length_required?:                 Net::HTTPLengthRequired,
        method_not_allowed?:              Net::HTTPMethodNotAllowed,
        moved_permanently?:               Net::HTTPMovedPermanently,
        multiple_choice?:                 Net::HTTPMultipleChoice,
        no_content?:                      Net::HTTPNoContent,
        non_authoritative_information?:   Net::HTTPNonAuthoritativeInformation,
        not_acceptable?:                  Net::HTTPNotAcceptable,
        not_found?:                       Net::HTTPNotFound,
        not_implemented?:                 Net::HTTPNotImplemented,
        not_modified?:                    Net::HTTPNotModified,
        ok?:                              Net::HTTPOK,
        partial_content?:                 Net::HTTPPartialContent,
        payment_required?:                Net::HTTPPaymentRequired,
        precondition_failed?:             Net::HTTPPreconditionFailed,
        proxy_authentication_required?:   Net::HTTPProxyAuthenticationRequired,
        request_entity_too_large?:        Net::HTTPRequestEntityTooLarge,
        request_time_out?:                Net::HTTPRequestTimeOut,
        request_uri_too_long?:            Net::HTTPRequestURITooLong,
        requested_range_not_satisfiable?: Net::HTTPRequestedRangeNotSatisfiable,
        reset_content?:                   Net::HTTPResetContent,
        see_other?:                       Net::HTTPSeeOther,
        service_unavailable?:             Net::HTTPServiceUnavailable,
        switch_protocol?:                 Net::HTTPSwitchProtocol,
        temporary_redirect?:              Net::HTTPTemporaryRedirect,
        unauthorized?:                    Net::HTTPUnauthorized,
        unsupported_media_type?:          Net::HTTPUnsupportedMediaType,
        use_proxy?:                       Net::HTTPUseProxy,
        version_not_supported?:           Net::HTTPVersionNotSupported
      }

      if ::RUBY_PLATFORM != "java"
        SPECIFIC_CODES[:multiple_choices?] = Net::HTTPMultipleChoices
      end

      # Ruby 2.6, those status codes have been updated.
      if ::RUBY_PLATFORM != "java"
        SPECIFIC_CODES[:gateway_timeout?]       = Net::HTTPGatewayTimeout
        SPECIFIC_CODES[:payload_too_large?]     = Net::HTTPPayloadTooLarge
        SPECIFIC_CODES[:request_timeout?]       = Net::HTTPRequestTimeout
        SPECIFIC_CODES[:uri_too_long?]          = Net::HTTPURITooLong
        SPECIFIC_CODES[:range_not_satisfiable?] = Net::HTTPRangeNotSatisfiable
      end

      SPECIFIC_CODES.each do |method, klass|
        it "responds to #{method}" do
          net_response = response_mock(klass)
          response = HTTParty::Response.new(@request_object, net_response, '')
          expect(response.__send__(method)).to be_truthy
        end
      end
    end
  end

  describe "headers" do
    let (:empty_headers) { HTTParty::Response::Headers.new }
    let (:some_headers_hash) do
      {'Cookie' => 'bob',
      'Content-Encoding' => 'meow'}
    end
    let (:some_headers) do
      HTTParty::Response::Headers.new.tap do |h|
        some_headers_hash.each_pair do |k,v|
          h[k] = v
        end
      end
    end
    it "can initialize without headers" do
      expect(empty_headers).to eq({})
    end

    it 'always equals itself' do
      expect(empty_headers).to eq(empty_headers)
      expect(some_headers).to eq(some_headers)
    end

    it 'does not equal itself when not equivalent' do
      expect(empty_headers).to_not eq(some_headers)
    end

    it 'does equal a hash' do
      expect(empty_headers).to eq({})

      expect(some_headers).to eq(some_headers_hash)
    end
  end

  describe "#tap" do
    it "is possible to tap into a response" do
      result = @response.tap(&:code)

      expect(result).to eq @response
    end
  end

  describe "#inspect" do
    it "works" do
      inspect = @response.inspect
      expect(inspect).to include("HTTParty::Response:0x")
      expect(inspect).to match(/parsed_response=.*\{.*foo.*=>.*bar.*\}/)
      expect(inspect).to include("@response=#<Net::HTTPOK 200 OK readbody=false>")
      expect(inspect).to include("@headers={")
      expect(inspect).to include("last-modified")
      expect(inspect).to include("content-length")
    end
  end

  describe 'marshalling' do
    before { RSpec::Mocks.space.proxy_for(@response_object).remove_stub(:body) }

    specify do
      marshalled = Marshal.load(Marshal.dump(@response))

      expect(marshalled.headers).to eq @response.headers
      expect(marshalled.body).to eq @response.body
      expect(marshalled.code).to eq @response.code
    end
  end
end
