require 'pact_broker/client/hal/http_client'
require "faraday"
require "faraday/retry"

module PactBroker::Client
  module Hal
    describe HttpClient do
      subject { HttpClient.new(username: 'foo', password: 'bar') }

      describe "get" do
        before do
          allow(subject).to receive(:until_truthy_or_max_times) { |&block| block.call }
        end

        let!(:request) do
          stub_request(:get, "http://example.org/").
            with(  headers: {
              'Accept'=>'application/hal+json',
              'Authorization'=>'Basic Zm9vOmJhcg=='
              }).
            to_return(status: 200, body: response_body, headers: {'Content-Type' => 'application/json'})
        end

        let(:response_body) { {some: 'json'}.to_json }
        let(:do_get) { subject.get('http://example.org') }

        it "performs a get request" do
          do_get
          expect(request).to have_been_made
        end

        context "with get params" do
          let!(:request) do
            stub_request(:get, "http://example.org/?foo=hello+world&bar=wiffle").
              to_return(status: 200)
          end

          let(:do_get) { subject.get('http://example.org', { 'foo' => 'hello world', 'bar' => 'wiffle' }) }

          it "correctly converts and encodes get params" do
             do_get
             expect(request).to have_been_made
          end
        end

        it "retries on failure" do
          expect(subject).to receive(:until_truthy_or_max_times)
          do_get
        end

        it "returns a response" do
          expect(do_get.body).to eq({"some" => "json"})
        end

      end

      describe "post" do
        before do
          allow(subject).to receive(:until_truthy_or_max_times) { |&block| block.call }
        end

        let!(:request) do
          stub_request(:post, "http://example.org/").
            with(  headers: {
              'Accept'=>'application/hal+json',
              'Authorization'=>'Basic Zm9vOmJhcg==',
              'Content-Type'=>'application/json'
              },
              body: request_body).
            to_return(status: 200, body: response_body, headers: {'Content-Type' => 'application/json'})
        end

        let(:request_body) { {some: 'data'}.to_json }
        let(:response_body) { {some: 'json'}.to_json }

        let(:do_post) { subject.post('http://example.org/', request_body) }

        it "performs a post request" do
          do_post
          expect(request).to have_been_made
        end

        it "calls Retry.until_truthy_or_max_times" do
          expect(subject).to receive(:until_truthy_or_max_times)
          do_post
        end

        it "returns a response" do
          expect(do_post.body).to eq({"some" => "json"})
        end

        context "with custom headers" do
          let!(:request) do
            stub_request(:post, "http://example.org/").
              with(  headers: {
                'Accept'=>'foo'
                }).
              to_return(status: 200)
          end

          let(:do_post) { subject.post('http://example.org/', request_body, {"Accept" => "foo"} ) }

          it "performs a post request with custom headers" do
            do_post
            expect(request).to have_been_made
          end
        end
      end

      describe "integration test" do
        before do
          allow(subject).to receive(:sleep)
        end

        let(:do_get) { subject.get('http://example.org') }

        context "with a 50x error is returned less than the max number of tries" do
          let!(:request) do
            stub_request(:get, "http://example.org").
              to_return({ status: 500 }, { status: 502 }, { status: 503 }, { status: 200 })
          end

          it "retries" do
            expect(do_get.status).to eq 200
          end
        end

        context "with a 50x error is returned more than the max number of tries" do
          let!(:request) do
            stub_request(:get, "http://example.org").
              to_return({ status: 500 }, { status: 501 }, { status: 502 }, { status: 503 }, { status: 504 })
          end

          it "retries and returns the last 50x response" do
            expect(do_get.status).to eq 504
          end
        end

        context "when exceptions are raised" do
          before do
            allow($stderr).to receive(:puts)
          end

          let!(:request) do
            stub_request(:get, "http://example.org")
              .to_raise(Errno::ECONNREFUSED)
          end

          it "logs the error" do
            expect($stderr).to receive(:puts).with(/Errno::ECONNREFUSED/)
            begin
              do_get
            rescue Errno::ECONNREFUSED
            end
          end

          it "retries and raises the last exception" do
            expect { do_get }.to raise_error(Errno::ECONNREFUSED)
          end
        end
      end

      describe "x509 certificate" do
        FAKE_SERVER_URL = 'https://localhost:4444'
        X509_CERT_FILE_PATH = './spec/fixtures/certificates/client_cert.pem'
        X509_KEY_FILE_PATH = './spec/fixtures/certificates/key.pem'
        UNSIGNED_X509_CERT_FILE_PATH = './spec/fixtures/certificates/unsigned_cert.pem'
        UNSIGNED_X509_KEY_FILE_PATH = './spec/fixtures/certificates/unsigned_key.pem'

        def wait_for_server_to_start
          Faraday.new(
            url: FAKE_SERVER_URL,
            ssl: {
              verify: false,
              client_cert: OpenSSL::X509::Certificate.new(File.read(X509_CERT_FILE_PATH)),
              client_key: OpenSSL::PKey::RSA.new(File.read(X509_KEY_FILE_PATH))
            }
          ) do |builder|
            builder.request :retry, max: 20, interval: 0.5, exceptions: [StandardError]
            builder.adapter :net_http
          end.get
        end

        let(:do_get) { subject.get(FAKE_SERVER_URL) }

        before(:all) do
          @pipe = IO.popen("bundle exec ruby ./spec/support/ssl_server.rb")
          ENV['SSL_CERT_FILE'] = "./spec/fixtures/certificates/ca_cert.pem"

          wait_for_server_to_start()
        end

        context "with valid x509 client certificates" do
          before do
            ENV['X509_CLIENT_CERT_FILE'] = X509_CERT_FILE_PATH
            ENV['X509_CLIENT_KEY_FILE'] = X509_KEY_FILE_PATH
          end

          it "succeeds" do
            expect(do_get.status).to eq 200
          end
        end

        context "when invalid x509 certificates are set" do
          before do
            ENV['X509_CLIENT_CERT_FILE'] = UNSIGNED_X509_CERT_FILE_PATH
            ENV['X509_CLIENT_KEY_FILE'] = UNSIGNED_X509_KEY_FILE_PATH
          end

          it "fails raising SSL error" do
            expect { do_get }
              .to raise_error { |error|
                expect([OpenSSL::SSL::SSLError, Errno::ECONNRESET]).to include(error.class)
              }
          end
        end

        context "when no x509 certificates are set" do
          before do
            ENV['X509_CLIENT_CERT_FILE'] = nil
            ENV['X509_CLIENT_KEY_FILE'] = nil
          end

          it "fails raising SSL error" do
            expect { do_get }
              .to raise_error { |error|
                expect([OpenSSL::SSL::SSLError, Errno::ECONNRESET]).to include(error.class)
              }
          end
        end

        after(:all) do
          Process.kill "KILL", @pipe.pid
        end
      end
    end
  end
end
