require 'spec_helper'
require 'acceptance/webmock_shared'
require 'ostruct'

unless RUBY_PLATFORM =~ /java/
  require 'acceptance/em_http_request/em_http_request_spec_helper'

  describe "EM::HttpRequest" do
    include EMHttpRequestSpecHelper

    before(:all) do
      skip 'em-http-request is not supported on Ruby >= 3.4' if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('3.4.0')
    end

    include_context "with WebMock", :no_status_message

    #functionality only supported for em-http-request 1.x
    if defined?(EventMachine::HttpConnection)
      context 'when a real request is made and redirects are followed', net_connect: true do
        before { WebMock.allow_net_connect! }

        # This url redirects to the https URL.
        let(:http_url) { "http://raw.github.com:80/gist/fb555cb593f3349d53af/6921dd638337d3f6a51b0e02e7f30e3c414f70d6/vcr_gist" }
        let(:https_url) { http_url.gsub('http', 'https').gsub('80', '443') }

        def make_request
          EM.run do
            request = EM::HttpRequest.new(http_url, ssl: {verify_peer: true}).get(redirects: 1)
            request.callback { EM.stop }
          end
        end

        it "invokes the globally_stub_request hook with both requests" do
          urls = []
          WebMock.globally_stub_request { |r| urls << r.uri.to_s; nil }

          make_request

          expect(urls).to eq([http_url, https_url])
        end

        it 'invokes the after_request hook with both requests' do
          urls = []
          WebMock.after_request { |req, res| urls << req.uri.to_s }

          make_request

          expect(urls).to eq([http_url, https_url])
        end
      end

      describe "with middleware" do

        it "should work with request middleware" do
          stub_request(:get, "www.example.com").with(body: 'bar')

          middleware = Class.new do
            def request(client, head, body)
              [{}, 'bar']
            end
          end

          EM.run do
            conn = EventMachine::HttpRequest.new('http://www.example.com/')

            conn.use middleware

            http = conn.get(body: 'foo')

            http.callback do
              expect(WebMock).to have_requested(:get, "www.example.com").with(body: 'bar')
              EM.stop
            end
          end
        end

        it "only calls request middleware once" do
          stub_request(:get, "www.example.com")

          middleware = Class.new do
            def self.called!
              @called = called + 1
            end

            def self.called
              @called || 0
            end

            def request(client, head, body)
              self.class.called!
              [head, body]
            end
          end

          EM.run do
            conn = EventMachine::HttpRequest.new('http://www.example.com/')
            conn.use middleware
            http = conn.get
            http.callback do
              expect(middleware.called).to eq(1)
              EM.stop
            end
          end
        end

        let(:response_middleware) do
          Class.new do
            def response(resp)
              resp.response = 'bar'
            end
          end
        end

        it "should work with response middleware" do
          stub_request(:get, "www.example.com").to_return(body: 'foo')

          EM.run do
            conn = EventMachine::HttpRequest.new('http://www.example.com/')

            conn.use response_middleware

            http = conn.get

            http.callback do
              expect(http.response).to eq('bar')
              EM.stop
            end
          end
        end

        let(:webmock_server_url) { "http://#{WebMockServer.instance.host_with_port}/" }

        shared_examples_for "em-http-request middleware/after_request hook integration" do
          it 'yields the original raw body to the after_request hook even if a response middleware modifies the body' do
            yielded_response_body = nil
            ::WebMock.after_request do |request, response|
              yielded_response_body = response.body
            end

            EM::HttpRequest.use response_middleware

            EM.run do
              http = EventMachine::HttpRequest.new(webmock_server_url).get
              http.callback { EM.stop }
            end

            expect(yielded_response_body).to eq("hello world")
          end
        end

        context 'making a real request', net_connect: true do
          before { WebMock.allow_net_connect! }
          include_examples "em-http-request middleware/after_request hook integration"

          it "doesn't modify headers" do
            EM.run do
              conn = EventMachine::HttpRequest.new(webmock_server_url)
              http = conn.post(head: { 'content-length' => '4' }, body: 'test')
              expect(conn).to receive(:send_data).with(/POST \/ HTTP\/1.1\r\nContent-Length: 4\r\nConnection: close\r\nHost: localhost:\d+\r\nUser-Agent: EventMachine HttpClient\r\nAccept-Encoding: gzip, compressed\r\n\r\n/).and_call_original
              expect(conn).to receive(:send_data).with('test')
              http.callback do
                EM.stop
              end
            end
          end

          it "only calls request middleware once" do
            middleware = Class.new do
              def self.called!
                @called = called + 1
              end

              def self.called
                @called || 0
              end

              def request(client, head, body)
                self.class.called!
                [head, body]
              end
            end

            EM.run do
              conn = EventMachine::HttpRequest.new(webmock_server_url)
              conn.use middleware
              http = conn.get
              http.callback do
                expect(middleware.called).to eq(1)
                EM.stop
              end
            end
          end
        end

        context 'when the request is stubbed' do
          before { stub_request(:get, webmock_server_url).to_return(body: 'hello world') }
          include_examples "em-http-request middleware/after_request hook integration"
        end
      end

      it 'should trigger error callbacks asynchronously' do
        stub_request(:get, 'www.example.com').to_timeout
        called = false

        EM.run do
          conn = EventMachine::HttpRequest.new('http://www.example.com/')
          http = conn.get
          http.errback do
            called = true
            EM.stop
          end
          expect(called).to eq(false)
        end

        expect(called).to eq(true)
      end

      # not pretty, but it works
      if defined?(EventMachine::Synchrony)
        describe "with synchrony" do
          let(:webmock_em_http) { File.expand_path(File.join(File.dirname(__FILE__), "../lib/webmock/http_lib_adapters/em_http_request/em_http_request_1_x.rb")) }

          before(:each) do
            # need to reload the webmock em-http adapter after we require synchrony
            WebMock::HttpLibAdapters::EmHttpRequestAdapter.disable!
            $".delete webmock_em_http
            $".delete File.expand_path(File.join(File.dirname(__FILE__), "../../../lib/webmock/http_lib_adapters/em_http_request_adapter.rb"))
            require 'em-synchrony'
            require 'em-synchrony/em-http'
            require File.expand_path(File.join(File.dirname(__FILE__), "../../../lib/webmock/http_lib_adapters/em_http_request_adapter.rb"))
          end

          it "should work" do
            stub_request(:post, /.*.testserver.com*/).to_return(status: 200, body: 'ok')
            expect {
              EM.run do
                fiber = Fiber.new do
                  EM::HttpRequest.new("http://www.testserver.com").post body: "foo=bar&baz=bang", timeout: 60
                  EM.stop
                end
                fiber.resume
              end
            }.not_to raise_error
          end

          after(:each) do
            EM.send(:remove_const, :Synchrony)
            EM.send(:remove_const, :HTTPMethods)
            WebMock::HttpLibAdapters::EmHttpRequestAdapter.disable!
            $".reject! {|path| path.include? "em-http-request"}
            $".delete webmock_em_http
            $".delete File.expand_path(File.join(File.dirname(__FILE__), "../../../lib/webmock/http_lib_adapters/em_http_request_adapter.rb"))
            require 'em-http-request'
            require File.expand_path(File.join(File.dirname(__FILE__), "../../../lib/webmock/http_lib_adapters/em_http_request_adapter.rb"))
          end
        end
      end
    end

    it "should work with streaming" do
      stub_request(:get, "www.example.com").to_return(body: "abc")
      response = ""
      EM.run {
        http = EventMachine::HttpRequest.new('http://www.example.com/').get
        http.stream { |chunk| response = chunk; EM.stop  }
      }
      expect(response).to eq("abc")
    end

    it "should work with responses that use chunked transfer encoding" do
      stub_request(:get, "www.example.com").to_return(body: "abc", headers: { 'Transfer-Encoding' => 'chunked' })
      expect(http_request(:get, "http://www.example.com").body).to eq("abc")
    end

    it "should work with optional query params" do
      stub_request(:get, "www.example.com/?x=3&a[]=b&a[]=c").to_return(body: "abc")
      expect(http_request(:get, "http://www.example.com/?x=3", query: {"a" => ["b", "c"]}).body).to eq("abc")
    end

    it "should work with optional query params declared as string" do
      stub_request(:get, "www.example.com/?x=3&a[]=b&a[]=c").to_return(body: "abc")
      expect(http_request(:get, "http://www.example.com/?x=3", query: "a[]=b&a[]=c").body).to eq("abc")
    end

    it "should work when the body is passed as a Hash" do
      stub_request(:post, "www.example.com").with(body: {a: "1", b: "2"}).to_return(body: "ok")
      expect(http_request(:post, "http://www.example.com", body: {a: "1", b: "2"}).body).to eq("ok")
    end

    if defined?(EventMachine::HttpConnection)
      it "should work when a file is passed as body" do
        stub_request(:post, "www.example.com").with(body: File.read(__FILE__)).to_return(body: "ok")
        expect(http_request(:post, "http://www.example.com", file: __FILE__).body).to eq("ok")
      end
    end

    it "should work with UTF-8 strings" do
      body = "Привет, Мир!"
      stub_request(:post, "www.example.com").to_return(body: body)
      expect(http_request(:post, "http://www.example.com").body.bytesize).to eq(body.bytesize)
    end

    it "should work with multiple requests to the same connection" do
      stub_request(:get, "www.example.com/foo").to_return(body: "bar")
      stub_request(:get, "www.example.com/baz").to_return(body: "wombat")
      err1  = nil
      err2  = nil
      body1 = nil
      body2 = nil
      i = 0

      EM.run do
        conn = EM::HttpRequest.new("http://www.example.com")
        conn.get(path: "/foo").callback do |resp|
          body1 = resp.response
          i += 1; EM.stop if i == 2
        end.errback do |resp|
          err1  = resp.error
          i += 1; EM.stop if i == 2
        end

        conn.get(path: "/baz").callback do |resp|
          body2 = resp.response
          i += 1; EM.stop if i == 2
        end.errback do |resp|
          err2  = resp.error
          i += 1; EM.stop if i == 2
        end
      end

      expect(err1).to be(nil)
      expect(err2).to be(nil)
      expect(body1).to eq("bar")
      expect(body2).to eq("wombat")
    end

    it "should work with multiple requests to the same connection when the first request times out" do
      stub_request(:get, "www.example.com/foo").to_timeout.then.to_return(status: 200, body: "wombat")
      err  = nil
      body = nil

      EM.run do
        conn = EM::HttpRequest.new("http://www.example.com")
        conn.get(path: "/foo").callback do |resp|
          err = :success_from_timeout
          EM.stop
        end.errback do |resp|
          conn.get(path: "/foo").callback do |retry_resp|
            expect(retry_resp.response_header.status).to eq(200)
            body = retry_resp.response
            EM.stop
          end.errback do |retry_resp|
            err = retry_resp.error
            EM.stop
          end
        end
      end

      expect(err).to be(nil)
      expect(body).to eq("wombat")
    end

    describe "mocking EM::HttpClient API" do
      let(:uri) { "http://www.example.com/" }

      before do
        stub_request(:get, uri)
        WebMock::HttpLibAdapters::EmHttpRequestAdapter.enable!
      end

      def client(uri, options = {})
        client = nil
        EM.run do
          client = EventMachine::HttpRequest.new(uri).get(options)
          client.callback { EM.stop }
          client.errback { failed }
        end
        client
      end

      subject { client(uri) }

      it 'should support #uri' do
        expect(subject.uri).to eq(Addressable::URI.parse(uri))
      end

      it 'should support #last_effective_url' do
        expect(subject.last_effective_url).to eq(Addressable::URI.parse(uri))
      end

      context "with a query" do
        let(:uri) { "http://www.example.com/?a=1&b=2" }
        subject { client("http://www.example.com/?a=1", query: { 'b' => 2 }) }

        it "#request_signature doesn't mutate the original uri" do
          expect(subject.uri).to eq(Addressable::URI.parse("http://www.example.com/?a=1"))
          signature = WebMock::RequestRegistry.instance.requested_signatures.hash.keys.first
          expect(signature.uri).to eq(Addressable::URI.parse(uri))
        end
      end

      describe 'get_response_cookie' do

        before(:each) do
          stub_request(:get, "http://example.org/").
          to_return(
            status: 200,
            body: "",
            headers: { 'Set-Cookie' => cookie_string }
          )
        end

        describe 'success' do

          context 'with only one cookie' do

            let(:cookie_name) { 'name_of_the_cookie' }
            let(:cookie_value) { 'value_of_the_cookie' }
            let(:cookie_string) { "#{cookie_name}=#{cookie_value}" }

            it 'successfully gets the cookie' do
              EM.run {
                http = EventMachine::HttpRequest.new('http://example.org').get

                http.errback { fail(http.error) }
                http.callback {
                  expect(http.get_response_cookie(cookie_name)).to eq(cookie_value)
                  EM.stop
                }
              }
            end
          end

          context 'with several cookies' do

            let(:cookie_name) { 'name_of_the_cookie' }
            let(:cookie_value) { 'value_of_the_cookie' }
            let(:cookie_2_name) { 'name_of_the_2nd_cookie' }
            let(:cookie_2_value) { 'value_of_the_2nd_cookie' }
            let(:cookie_string) { %W(#{cookie_name}=#{cookie_value} #{cookie_2_name}=#{cookie_2_value}) }

            it 'successfully gets both cookies' do
              EM.run {
                http = EventMachine::HttpRequest.new('http://example.org').get

                http.errback { fail(http.error) }
                http.callback {
                  expect(http.get_response_cookie(cookie_name)).to eq(cookie_value)
                  expect(http.get_response_cookie(cookie_2_name)).to eq(cookie_2_value)
                  EM.stop
                }
              }
            end
          end
        end

        describe 'failure' do

          let(:cookie_string) { 'a=b' }

          it 'returns nil when no cookie is found' do
            EM.run {
                http = EventMachine::HttpRequest.new('http://example.org').get

                http.errback { fail(http.error) }
                http.callback {
                  expect(http.get_response_cookie('not_found_cookie')).to eq(nil)
                  EM.stop
                }
              }
          end
        end
      end
    end

  end
end
