# frozen_string_literal: true

shared_examples 'initializer with url' do
  context 'with simple url' do
    let(:address) { 'http://sushi.com' }

    it { expect(subject.host).to eq('sushi.com') }
    it { expect(subject.port).to eq(80) }
    it { expect(subject.scheme).to eq('http') }
    it { expect(subject.path_prefix).to eq('/') }
    it { expect(subject.params).to eq({}) }
  end

  context 'with complex url' do
    let(:address) { 'http://sushi.com:815/fish?a=1' }

    it { expect(subject.port).to eq(815) }
    it { expect(subject.path_prefix).to eq('/fish') }
    it { expect(subject.params).to eq('a' => '1') }
  end

  context 'with IPv6 address' do
    let(:address) { 'http://[::1]:85/' }

    it { expect(subject.host).to eq('[::1]') }
    it { expect(subject.port).to eq(85) }
  end
end

shared_examples 'default connection options' do
  after { Faraday.default_connection_options = nil }

  it 'works with implicit url' do
    conn = Faraday.new 'http://sushi.com/foo'
    expect(conn.options.timeout).to eq(10)
  end

  it 'works with option url' do
    conn = Faraday.new url: 'http://sushi.com/foo'
    expect(conn.options.timeout).to eq(10)
  end

  it 'works with instance connection options' do
    conn = Faraday.new 'http://sushi.com/foo', request: { open_timeout: 1 }
    expect(conn.options.timeout).to eq(10)
    expect(conn.options.open_timeout).to eq(1)
  end

  it 'default connection options persist with an instance overriding' do
    conn = Faraday.new 'http://nigiri.com/bar'
    conn.options.timeout = 1
    expect(Faraday.default_connection_options.request.timeout).to eq(10)

    other = Faraday.new url: 'https://sushi.com/foo'
    other.options.timeout = 1

    expect(Faraday.default_connection_options.request.timeout).to eq(10)
  end

  it 'default connection uses default connection options' do
    expect(Faraday.default_connection.options.timeout).to eq(10)
  end
end

RSpec.describe Faraday::Connection do
  let(:conn) { Faraday::Connection.new(url, options) }
  let(:url) { nil }
  let(:options) { nil }

  describe '.new' do
    subject { conn }

    context 'with implicit url param' do
      # Faraday::Connection.new('http://sushi.com')
      let(:url) { address }

      it_behaves_like 'initializer with url'
    end

    context 'with explicit url param' do
      # Faraday::Connection.new(url: 'http://sushi.com')
      let(:url) { { url: address } }

      it_behaves_like 'initializer with url'
    end

    context 'with custom builder' do
      let(:custom_builder) { Faraday::RackBuilder.new }
      let(:options) { { builder: custom_builder } }

      it { expect(subject.builder).to eq(custom_builder) }
    end

    context 'with custom params' do
      let(:options) { { params: { a: 1 } } }

      it { expect(subject.params).to eq('a' => 1) }
    end

    context 'with custom params and params in url' do
      let(:url) { 'http://sushi.com/fish?a=1&b=2' }
      let(:options) { { params: { a: 3 } } }
      it { expect(subject.params).to eq('a' => 3, 'b' => '2') }
    end

    context 'with custom headers' do
      let(:options) { { headers: { user_agent: 'Faraday' } } }

      it { expect(subject.headers['User-agent']).to eq('Faraday') }
    end

    context 'with ssl false' do
      let(:options) { { ssl: { verify: false } } }

      it { expect(subject.ssl.verify?).to be_falsey }
    end

    context 'with empty block' do
      let(:conn) { Faraday::Connection.new {} }

      it { expect(conn.builder.handlers.size).to eq(0) }
    end

    context 'with block' do
      let(:conn) do
        Faraday::Connection.new(params: { 'a' => '1' }) do |faraday|
          faraday.adapter :net_http
          faraday.url_prefix = 'http://sushi.com/omnom'
        end
      end

      it { expect(conn.builder.handlers.size).to eq(0) }
      it { expect(conn.path_prefix).to eq('/omnom') }
    end
  end

  describe '#close' do
    it 'can close underlying app' do
      expect(conn.app).to receive(:close)
      conn.close
    end
  end

  describe 'basic_auth' do
    subject { conn }

    context 'calling the #basic_auth method' do
      before { subject.basic_auth 'Aladdin', 'open sesame' }

      it { expect(subject.headers['Authorization']).to eq('Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==') }
    end

    context 'adding basic auth info to url' do
      let(:url) { 'http://Aladdin:open%20sesame@sushi.com/fish' }

      it { expect(subject.headers['Authorization']).to eq('Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==') }
    end
  end

  describe '#token_auth' do
    before { subject.token_auth('abcdef', nonce: 'abc') }

    it { expect(subject.headers['Authorization']).to eq('Token nonce="abc", token="abcdef"') }
  end

  describe '#build_exclusive_url' do
    context 'with relative path' do
      subject { conn.build_exclusive_url('sake.html') }

      it 'uses connection host as default host' do
        conn.host = 'sushi.com'
        expect(subject.host).to eq('sushi.com')
        expect(subject.scheme).to eq('http')
      end

      it do
        conn.path_prefix = '/fish'
        expect(subject.path).to eq('/fish/sake.html')
      end

      it do
        conn.path_prefix = '/'
        expect(subject.path).to eq('/sake.html')
      end

      it do
        conn.path_prefix = 'fish'
        expect(subject.path).to eq('/fish/sake.html')
      end

      it do
        conn.path_prefix = '/fish/'
        expect(subject.path).to eq('/fish/sake.html')
      end
    end

    context 'with absolute path' do
      subject { conn.build_exclusive_url('/sake.html') }

      after { expect(subject.path).to eq('/sake.html') }

      it { conn.path_prefix = '/fish' }
      it { conn.path_prefix = '/' }
      it { conn.path_prefix = 'fish' }
      it { conn.path_prefix = '/fish/' }
    end

    context 'with complete url' do
      subject { conn.build_exclusive_url('http://sushi.com/sake.html?a=1') }

      it { expect(subject.scheme).to eq('http') }
      it { expect(subject.host).to eq('sushi.com') }
      it { expect(subject.port).to eq(80) }
      it { expect(subject.path).to eq('/sake.html') }
      it { expect(subject.query).to eq('a=1') }
    end

    it 'overrides connection port for absolute url' do
      conn.port = 23
      uri = conn.build_exclusive_url('http://sushi.com')
      expect(uri.port).to eq(80)
    end

    it 'does not add ending slash given nil url' do
      conn.url_prefix = 'http://sushi.com/nigiri'
      uri = conn.build_exclusive_url
      expect(uri.path).to eq('/nigiri')
    end

    it 'does not add ending slash given empty url' do
      conn.url_prefix = 'http://sushi.com/nigiri'
      uri = conn.build_exclusive_url('')
      expect(uri.path).to eq('/nigiri')
    end

    it 'does not use connection params' do
      conn.url_prefix = 'http://sushi.com/nigiri'
      conn.params = { a: 1 }
      expect(conn.build_exclusive_url.to_s).to eq('http://sushi.com/nigiri')
    end

    it 'allows to provide params argument' do
      conn.url_prefix = 'http://sushi.com/nigiri'
      conn.params = { a: 1 }
      params = Faraday::Utils::ParamsHash.new
      params[:a] = 2
      uri = conn.build_exclusive_url(nil, params)
      expect(uri.to_s).to eq('http://sushi.com/nigiri?a=2')
    end

    it 'handles uri instances' do
      uri = conn.build_exclusive_url(URI('/sake.html'))
      expect(uri.path).to eq('/sake.html')
    end

    it 'always returns new URI instance' do
      conn.url_prefix = 'http://sushi.com'
      uri1 = conn.build_exclusive_url(nil)
      uri2 = conn.build_exclusive_url(nil)
      expect(uri1).not_to equal(uri2)
    end

    context 'with url_prefixed connection' do
      let(:url) { 'http://sushi.com/sushi/' }

      it 'parses url and changes scheme' do
        conn.scheme = 'https'
        uri = conn.build_exclusive_url('sake.html')
        expect(uri.to_s).to eq('https://sushi.com/sushi/sake.html')
      end

      it 'joins url to base with ending slash' do
        uri = conn.build_exclusive_url('sake.html')
        expect(uri.to_s).to eq('http://sushi.com/sushi/sake.html')
      end

      it 'used default base with ending slash' do
        uri = conn.build_exclusive_url
        expect(uri.to_s).to eq('http://sushi.com/sushi/')
      end

      it 'overrides base' do
        uri = conn.build_exclusive_url('/sake/')
        expect(uri.to_s).to eq('http://sushi.com/sake/')
      end
    end

    context 'with colon in path' do
      let(:url) { 'http://service.com' }

      it 'joins url to base when used absolute path' do
        conn = Faraday.new(url: url)
        uri = conn.build_exclusive_url('/service:search?limit=400')
        expect(uri.to_s).to eq('http://service.com/service:search?limit=400')
      end

      it 'joins url to base when used relative path' do
        conn = Faraday.new(url: url)
        uri = conn.build_exclusive_url('service:search?limit=400')
        expect(uri.to_s).to eq('http://service.com/service%3Asearch?limit=400')
      end

      it 'joins url to base when used with path prefix' do
        conn = Faraday.new(url: url)
        conn.path_prefix = '/api'
        uri = conn.build_exclusive_url('service:search?limit=400')
        expect(uri.to_s).to eq('http://service.com/api/service%3Asearch?limit=400')
      end
    end
  end

  describe '#build_url' do
    let(:url) { 'http://sushi.com/nigiri' }

    it 'uses params' do
      conn.params = { a: 1, b: 1 }
      expect(conn.build_url.to_s).to eq('http://sushi.com/nigiri?a=1&b=1')
    end

    it 'merges params' do
      conn.params = { a: 1, b: 1 }
      url = conn.build_url(nil, b: 2, c: 3)
      expect(url.to_s).to eq('http://sushi.com/nigiri?a=1&b=2&c=3')
    end
  end

  describe '#build_request' do
    let(:url) { 'https://asushi.com/sake.html' }
    let(:request) { conn.build_request(:get) }

    before do
      conn.headers = { 'Authorization' => 'token abc123' }
      request.headers.delete('Authorization')
    end

    it { expect(conn.headers.keys).to eq(['Authorization']) }
    it { expect(conn.headers.include?('Authorization')).to be_truthy }
    it { expect(request.headers.keys).to be_empty }
    it { expect(request.headers.include?('Authorization')).to be_falsey }
  end

  describe '#to_env' do
    subject { conn.build_request(:get).to_env(conn).url }

    let(:url) { 'http://sushi.com/sake.html' }
    let(:options) { { params: @params } }

    it 'parses url params into query' do
      @params = { 'a[b]' => '1 + 2' }
      expect(subject.query).to eq('a%5Bb%5D=1+%2B+2')
    end

    it 'escapes per spec' do
      @params = { 'a' => '1+2 foo~bar.-baz' }
      expect(subject.query).to eq('a=1%2B2+foo~bar.-baz')
    end

    it 'bracketizes nested params in query' do
      @params = { 'a' => { 'b' => 'c' } }
      expect(subject.query).to eq('a%5Bb%5D=c')
    end

    it 'bracketizes repeated params in query' do
      @params = { 'a' => [1, 2] }
      expect(subject.query).to eq('a%5B%5D=1&a%5B%5D=2')
    end

    it 'without braketizing repeated params in query' do
      @params = { 'a' => [1, 2] }
      conn.options.params_encoder = Faraday::FlatParamsEncoder
      expect(subject.query).to eq('a=1&a=2')
    end
  end

  describe 'proxy support' do
    it 'accepts string' do
      with_env 'http_proxy' => 'http://env-proxy.com:80' do
        conn.proxy = 'http://proxy.com'
        expect(conn.proxy.host).to eq('proxy.com')
      end
    end

    it 'accepts uri' do
      with_env 'http_proxy' => 'http://env-proxy.com:80' do
        conn.proxy = URI.parse('http://proxy.com')
        expect(conn.proxy.host).to eq('proxy.com')
      end
    end

    it 'accepts hash with string uri' do
      with_env 'http_proxy' => 'http://env-proxy.com:80' do
        conn.proxy = { uri: 'http://proxy.com', user: 'rick' }
        expect(conn.proxy.host).to eq('proxy.com')
        expect(conn.proxy.user).to eq('rick')
      end
    end

    it 'accepts hash' do
      with_env 'http_proxy' => 'http://env-proxy.com:80' do
        conn.proxy = { uri: URI.parse('http://proxy.com'), user: 'rick' }
        expect(conn.proxy.host).to eq('proxy.com')
        expect(conn.proxy.user).to eq('rick')
      end
    end

    it 'accepts http env' do
      with_env 'http_proxy' => 'http://env-proxy.com:80' do
        expect(conn.proxy.host).to eq('env-proxy.com')
      end
    end

    it 'accepts http env with auth' do
      with_env 'http_proxy' => 'http://a%40b:my%20pass@proxy.com:80' do
        expect(conn.proxy.user).to eq('a@b')
        expect(conn.proxy.password).to eq('my pass')
      end
    end

    it 'accepts env without scheme' do
      with_env 'http_proxy' => 'localhost:8888' do
        uri = conn.proxy[:uri]
        expect(uri.host).to eq('localhost')
        expect(uri.port).to eq(8888)
      end
    end

    it 'fetches no proxy from nil env' do
      with_env 'http_proxy' => nil do
        expect(conn.proxy).to be_nil
      end
    end

    it 'fetches no proxy from blank env' do
      with_env 'http_proxy' => '' do
        expect(conn.proxy).to be_nil
      end
    end

    it 'does not accept uppercase env' do
      with_env 'HTTP_PROXY' => 'http://localhost:8888/' do
        expect(conn.proxy).to be_nil
      end
    end

    it 'allows when url in no proxy list' do
      with_env 'http_proxy' => 'http://proxy.com', 'no_proxy' => 'example.com' do
        conn = Faraday::Connection.new('http://example.com')
        expect(conn.proxy).to be_nil
      end
    end

    it 'allows when url in no proxy list with url_prefix' do
      with_env 'http_proxy' => 'http://proxy.com', 'no_proxy' => 'example.com' do
        conn = Faraday::Connection.new
        conn.url_prefix = 'http://example.com'
        expect(conn.proxy).to be_nil
      end
    end

    it 'allows when prefixed url is not in no proxy list' do
      with_env 'http_proxy' => 'http://proxy.com', 'no_proxy' => 'example.com' do
        conn = Faraday::Connection.new('http://prefixedexample.com')
        expect(conn.proxy.host).to eq('proxy.com')
      end
    end

    it 'allows when subdomain url is in no proxy list' do
      with_env 'http_proxy' => 'http://proxy.com', 'no_proxy' => 'example.com' do
        conn = Faraday::Connection.new('http://subdomain.example.com')
        expect(conn.proxy).to be_nil
      end
    end

    it 'allows when url not in no proxy list' do
      with_env 'http_proxy' => 'http://proxy.com', 'no_proxy' => 'example2.com' do
        conn = Faraday::Connection.new('http://example.com')
        expect(conn.proxy.host).to eq('proxy.com')
      end
    end

    it 'allows when ip address is not in no proxy list but url is' do
      with_env 'http_proxy' => 'http://proxy.com', 'no_proxy' => 'localhost' do
        conn = Faraday::Connection.new('http://127.0.0.1')
        expect(conn.proxy).to be_nil
      end
    end

    it 'allows when url is not in no proxy list but ip address is' do
      with_env 'http_proxy' => 'http://proxy.com', 'no_proxy' => '127.0.0.1' do
        conn = Faraday::Connection.new('http://localhost')
        expect(conn.proxy).to be_nil
      end
    end

    it 'allows in multi element no proxy list' do
      with_env 'http_proxy' => 'http://proxy.com', 'no_proxy' => 'example0.com,example.com,example1.com' do
        expect(Faraday::Connection.new('http://example0.com').proxy).to be_nil
        expect(Faraday::Connection.new('http://example.com').proxy).to be_nil
        expect(Faraday::Connection.new('http://example1.com').proxy).to be_nil
        expect(Faraday::Connection.new('http://example2.com').proxy.host).to eq('proxy.com')
      end
    end

    it 'test proxy requires uri' do
      expect { conn.proxy = { uri: :bad_uri, user: 'rick' } }.to raise_error(ArgumentError)
    end

    it 'uses env http_proxy' do
      with_env 'http_proxy' => 'http://proxy.com' do
        conn = Faraday.new
        expect(conn.instance_variable_get('@manual_proxy')).to be_falsey
        expect(conn.proxy_for_request('http://google.co.uk').host).to eq('proxy.com')
      end
    end

    it 'uses processes no_proxy before http_proxy' do
      with_env 'http_proxy' => 'http://proxy.com', 'no_proxy' => 'google.co.uk' do
        conn = Faraday.new
        expect(conn.instance_variable_get('@manual_proxy')).to be_falsey
        expect(conn.proxy_for_request('http://google.co.uk')).to be_nil
      end
    end

    it 'uses env https_proxy' do
      with_env 'https_proxy' => 'https://proxy.com' do
        conn = Faraday.new
        expect(conn.instance_variable_get('@manual_proxy')).to be_falsey
        expect(conn.proxy_for_request('https://google.co.uk').host).to eq('proxy.com')
      end
    end

    it 'uses processes no_proxy before https_proxy' do
      with_env 'https_proxy' => 'https://proxy.com', 'no_proxy' => 'google.co.uk' do
        conn = Faraday.new
        expect(conn.instance_variable_get('@manual_proxy')).to be_falsey
        expect(conn.proxy_for_request('https://google.co.uk')).to be_nil
      end
    end

    it 'gives priority to manually set proxy' do
      with_env 'https_proxy' => 'https://proxy.com', 'no_proxy' => 'google.co.uk' do
        conn = Faraday.new
        conn.proxy = 'http://proxy2.com'

        expect(conn.instance_variable_get('@manual_proxy')).to be_truthy
        expect(conn.proxy_for_request('https://google.co.uk').host).to eq('proxy2.com')
      end
    end

    it 'ignores env proxy if set that way' do
      with_env_proxy_disabled do
        with_env 'http_proxy' => 'http://duncan.proxy.com:80' do
          expect(conn.proxy).to be_nil
        end
      end
    end

    context 'performing a request' do
      before { stub_request(:get, 'http://example.com') }

      it 'dynamically checks proxy' do
        with_env 'http_proxy' => 'http://proxy.com:80' do
          conn = Faraday.new
          expect(conn.proxy.uri.host).to eq('proxy.com')

          conn.get('http://example.com') do |req|
            expect(req.options.proxy.uri.host).to eq('proxy.com')
          end
        end

        conn.get('http://example.com')
        expect(conn.instance_variable_get('@temp_proxy')).to be_nil
      end

      it 'dynamically check no proxy' do
        with_env 'http_proxy' => 'http://proxy.com', 'no_proxy' => 'example.com' do
          conn = Faraday.new

          expect(conn.proxy.uri.host).to eq('proxy.com')

          conn.get('http://example.com') do |req|
            expect(req.options.proxy).to be_nil
          end
        end
      end
    end
  end

  describe '#dup' do
    subject { conn.dup }

    let(:url) { 'http://sushi.com/foo' }
    let(:options) do
      {
        ssl: { verify: :none },
        headers: { 'content-type' => 'text/plain' },
        params: { 'a' => '1' },
        request: { timeout: 5 }
      }
    end

    it { expect(subject.build_exclusive_url).to eq(conn.build_exclusive_url) }
    it { expect(subject.headers['content-type']).to eq('text/plain') }
    it { expect(subject.params['a']).to eq('1') }

    context 'after manual changes' do
      before do
        subject.basic_auth('', '')
        subject.headers['content-length'] = 12
        subject.params['b'] = '2'
        subject.options[:open_timeout] = 10
      end

      it { expect(subject.builder.handlers.size).to eq(1) }
      it { expect(conn.builder.handlers.size).to eq(1) }
      it { expect(conn.headers.key?('content-length')).to be_falsey }
      it { expect(conn.params.key?('b')).to be_falsey }
      it { expect(subject.options[:timeout]).to eq(5) }
      it { expect(conn.options[:open_timeout]).to be_nil }
    end
  end

  describe '#respond_to?' do
    it { expect(Faraday.respond_to?(:get)).to be_truthy }
    it { expect(Faraday.respond_to?(:post)).to be_truthy }
  end

  describe 'default_connection_options' do
    context 'assigning a default value' do
      before do
        Faraday.default_connection_options = nil
        Faraday.default_connection_options.request.timeout = 10
      end

      it_behaves_like 'default connection options'
    end

    context 'assigning a hash' do
      before { Faraday.default_connection_options = { request: { timeout: 10 } } }

      it_behaves_like 'default connection options'
    end
  end

  describe 'request params' do
    context 'with simple url' do
      let(:url) { 'http://example.com' }
      let!(:stubbed) { stub_request(:get, 'http://example.com?a=a&p=3') }

      after { expect(stubbed).to have_been_made.once }

      it 'test_overrides_request_params' do
        conn.get('?p=2&a=a', p: 3)
      end

      it 'test_overrides_request_params_block' do
        conn.get('?p=1&a=a', p: 2) do |req|
          req.params[:p] = 3
        end
      end

      it 'test_overrides_request_params_block_url' do
        conn.get(nil, p: 2) do |req|
          req.url('?p=1&a=a', 'p' => 3)
        end
      end
    end

    context 'with url and extra params' do
      let(:url) { 'http://example.com?a=1&b=2' }
      let(:options) { { params: { c: 3 } } }

      it 'merges connection and request params' do
        stubbed = stub_request(:get, 'http://example.com?a=1&b=2&c=3&limit=5&page=1')
        conn.get('?page=1', limit: 5)
        expect(stubbed).to have_been_made.once
      end

      it 'allows to override all params' do
        stubbed = stub_request(:get, 'http://example.com?b=b')
        conn.get('?p=1&a=a', p: 2) do |req|
          expect(req.params[:a]).to eq('a')
          expect(req.params['c']).to eq(3)
          expect(req.params['p']).to eq(2)
          req.params = { b: 'b' }
          expect(req.params['b']).to eq('b')
        end
        expect(stubbed).to have_been_made.once
      end

      it 'allows to set params_encoder for single request' do
        encoder = Object.new
        def encoder.encode(params)
          params.map { |k, v| "#{k.upcase}-#{v.to_s.upcase}" }.join(',')
        end
        stubbed = stub_request(:get, 'http://example.com/?A-1,B-2,C-3,FEELING-BLUE')

        conn.get('/', feeling: 'blue') do |req|
          req.options.params_encoder = encoder
        end
        expect(stubbed).to have_been_made.once
      end
    end

    context 'with default params encoder' do
      let!(:stubbed) { stub_request(:get, 'http://example.com?color%5B%5D=red&color%5B%5D=blue') }
      after { expect(stubbed).to have_been_made.once }

      it 'supports array params in url' do
        conn.get('http://example.com?color[]=red&color[]=blue')
      end

      it 'supports array params in params' do
        conn.get('http://example.com', color: %w[red blue])
      end
    end

    context 'with flat params encoder' do
      let(:options) { { request: { params_encoder: Faraday::FlatParamsEncoder } } }
      let!(:stubbed) { stub_request(:get, 'http://example.com?color=blue') }
      after { expect(stubbed).to have_been_made.once }

      it 'supports array params in params' do
        conn.get('http://example.com', color: %w[red blue])
      end

      context 'with array param in url' do
        let(:url) { 'http://example.com?color[]=red&color[]=blue' }

        it do
          conn.get('/')
        end
      end
    end
  end
end
