require 'spec_helper'

describe Sand::Client do
  let(:client) { Sand::Client.new(client_id: 'a', client_secret: 'b', token_site: 'http://localhost', token_path: '/abc', default_retry_count: 2, cache: Sand::Memory.cache) }
  after { client.cache.clear }

  describe '#request' do
    let(:response) { 'good' }
    subject do
      client.request(cache_key: 'test') { |token| response }
    end
    before{ allow(client).to receive(:token).and_return('abc') }

    context 'with authorized response' do
      it 'returns the response' do
        allow(response).to receive(:status).and_return(200)
        expect(subject).to eq(response)

        allow(response).to receive(:code).and_return(200)
        expect(subject).to eq(response)
      end
    end

    context 'with unknown response code' do
      it 'returns the response' do
        expect(subject).to eq(response)
      end
    end

    context 'with unauthorized response' do
      before{ allow(response).to receive(:code).and_return(401) }

      context 'with retry' do
        it 'performs retry and returns 401 response' do
          expect(client).to receive(:sleep).exactly(2).times
          expect(subject.code).to eq(401)
        end
      end

      context 'with default retry set to 0' do
        subject do
          client.default_retry_count = 0
          client.request(cache_key: 'test') { |token| response }
        end
        it 'still performs retry once and returns 401 response' do
          expect(client).to receive(:sleep).exactly(1).times
          expect(subject.code).to eq(401)
        end
      end

      context 'with per-request retry' do
        subject do
          client.default_retry_count = 0
          client.request(cache_key: 'test', num_retry: 2) { |token| response }
        end
        it 'performs retry and returns 401 response' do
          expect(client).to receive(:sleep).exactly(2).times
          expect(subject.code).to eq(401)
        end
      end
    end
  end

  describe '#token' do
    let(:key) { 'test' }
    subject{ client.token(cache_key: key, scopes: ['scope']) }
    before{ allow(client).to receive(:oauth_token).and_return({ access_token: 'retrieve_token', expires_in: 120 }) }

    describe 'reading from cache' do
      before{ client.cache.write(client.cache_key('test', ['scope']), 'testToken', 60) }

      it 'uses the token from cache' do
        expect(client).not_to receive(:oauth_token)
        expect(subject).to eq('testToken')
      end

      context 'with key not found in cache' do
        let(:key) { 'not_test' }

        it 'retrieves the token from SAND' do
          expect(client).to receive(:oauth_token)
          expect(subject).not_to eq('testToken')
        end
      end
    end

    describe 'writing to cache' do
      it 'writes the token to cache' do
        expect(subject).to eq('retrieve_token')
        expect(client.cache.read(client.cache_key(key, ['scope'], nil))).to eq('retrieve_token')
      end
    end
  end

  describe '#token_with_expiry' do
    let(:key) { 'test' }
    subject{ client.token_with_expiry(cache_key: key, scopes: ['scope']) }
    before{ allow(client).to receive(:oauth_token).and_return({access_token: 'token_from_sand', expires_in: 120}) }

    context 'with token in the cache' do
      before{ client.cache.write(client.cache_key('test', ['scope']), 'token_in_cache', 60) }

      it 'uses the token from cache' do
        expect(client).not_to receive(:oauth_token)
        expect(client.cache).to receive(:read_raw).and_call_original
        expect(subject[:access_token]).to eq('token_in_cache')

        # Call again, should still use cache
        expect(client.cache).to receive(:read_raw).and_call_original
        result = client.token_with_expiry(cache_key: key, scopes: ['scope'])
        expect(result[:access_token]).to eq('token_in_cache')
      end

      context 'with use cached token option disabled' do
        subject{ client.token_with_expiry(cache_key: key, scopes: ['scope'], use_cached_token: false) }

        it 'does not use token from cache' do
          data = { access_token: 'new_token', expires_in: 30 }
          expect(client.cache).not_to receive(:read)
          expect(client.cache).to receive(:write).and_return({ expires_at_epoch_time: 100 })
          expect(client).to receive(:oauth_token).and_return(data)
          expect(subject).to eq({ access_token: 'new_token', expires_at_epoch_time: 100 })
        end
      end
    end

    context 'with token in cache has expired' do
      let(:time) { Time.now.to_i - 10 }
      let(:data) do
        {
          data: 'cached_token',
          expires_at_epoch_time: time
        }
      end
      let(:cach_key) { client.cache_key(key, ['scope'], nil) }
      before{ client.cache.primary.write(cach_key, data, expires_in: 10) }

      it 'gets token from Sand and writes the token to cache' do
        expect(client.cache).to receive(:write).and_call_original
        expect(subject[:access_token]).to eq('token_from_sand')
        expect(client.cache.read(cach_key)).to eq('token_from_sand')

        # Call the same method again, we should get the token from cache
        expect(client).not_to receive(:oauth_token)
        result = client.token_with_expiry(cache_key: key, scopes: ['scope'])
        expect(result[:access_token]).to eq('token_from_sand')
      end
    end

    context 'without token in the cache' do
      let(:data) { { access_token: 'token_from_sand', expires_in: 60 } }

      it 'gets token from Sand' do
        expect(client).to receive(:oauth_token).and_return(data)
        expect(subject[:access_token]).to eq('token_from_sand')

        # Now token is in cache, so next call should come from cache
        expect(client).not_to receive(:oauth_token)
        result = client.token_with_expiry(cache_key: key, scopes: ['scope'])
        expect(result[:access_token]).to eq('token_from_sand')
      end
    end
  end

  describe '#oauth_token' do
    context 'gets token successfully' do
      before { allow_any_instance_of(OAuth2::Strategy::ClientCredentials).to receive(:get_token).and_return(Token.new('token', 60)) }

      it 'returns token and expiry time' do
        t = client.oauth_token(scopes: ['test', 'scope'])
        expect(t[:access_token]).to eq('token')
        expect(t[:expires_in]).to eq(60 - Sand::Client::TOKEN_COMPENSATE_MARGIN)
      end
    end

    context 'on forbidden error' do
      before do
        error = StandardError.new("forbidden error")
        allow_any_instance_of(StandardError).to receive(:response).and_return(Faraday::Response.new(status: 403))
        allow_any_instance_of(OAuth2::Strategy::ClientCredentials).to receive(:get_token).and_raise(error)
      end


      context 'with retry' do
        it 'should raise error and not call sleep' do
          client.default_retry_count = 2
          expect(client).not_to receive(:sleep)
          expect{client.oauth_token}.to raise_error(StandardError)
        end
      end

      context 'without retry' do
        it 'should raise error and not call sleep' do
          client.default_retry_count = 0
          expect(client).not_to receive(:sleep)
          expect{client.oauth_token}.to raise_error(StandardError)
        end
      end
    end

    context 'on network error' do
      before { allow_any_instance_of(OAuth2::Strategy::ClientCredentials).to receive(:get_token).and_raise(StandardError) }

      context 'without retry' do
        it 'should raise error and not call sleep' do
          client.default_retry_count = 0
          expect(client).not_to receive(:sleep)
          expect{client.oauth_token}.to raise_error(StandardError)
        end
      end

      context 'with retry' do
        it 'should call sleep and then raise error' do
          client.default_retry_count = 2
          expect(client).to receive(:sleep).exactly(2).times
          expect{client.oauth_token}.to raise_error(StandardError)
        end
      end

      context 'with per-request retry' do
        it 'should call sleep and then raise error' do
          client.default_retry_count = 0
          expect(client).to receive(:sleep).exactly(3).times
          expect{client.oauth_token(num_retry: 3)}.to raise_error(StandardError)
        end

        context 'with per-request retry set to 0' do
          it 'should not retry' do
            client.default_retry_count = 3
            expect(client).to receive(:sleep).exactly(0).times
            expect{client.oauth_token(num_retry: 0)}.to raise_error(StandardError)
          end
        end

        context 'with per-request retry set to nil or a negative number' do
          it 'should default to default_retry_count for number of retries' do
            client.default_retry_count = 3
            expect(client).to receive(:sleep).exactly(3).times
            expect{client.oauth_token(num_retry: -1)}.to raise_error(StandardError)
          end

          it 'should default to default_retry_count for number of retries' do
            client.default_retry_count = 3
            expect(client).to receive(:sleep).exactly(3).times
            expect{client.oauth_token(num_retry: nil)}.to raise_error(StandardError)
          end
        end
      end

      context 'empty token from Sand' do
        it 'retries and raises an error' do
          allow_any_instance_of(OAuth2::Strategy::ClientCredentials).to receive(:get_token).and_return(Token.new('', 60))
          expect(client).to receive(:sleep).exactly(2).times
          expect{ client.oauth_token(num_retry: 2) }.to raise_error(Sand::InternalServerError, 'Invalid token data from Sand')

          allow_any_instance_of(OAuth2::Strategy::ClientCredentials).to receive(:get_token).and_return(Token.new(nil, 60))
          expect(client).to receive(:sleep).exactly(2).times
          expect{ client.oauth_token(num_retry: 2) }.to raise_error(Sand::InternalServerError, 'Invalid token data from Sand')

          allow_any_instance_of(OAuth2::Strategy::ClientCredentials).to receive(:get_token).and_return(Token.new('s', nil))
          expect(client).to receive(:sleep).exactly(2).times
          expect{ client.oauth_token(num_retry: 2) }.to raise_error(Sand::InternalServerError, 'Invalid token data from Sand')

          allow_any_instance_of(OAuth2::Strategy::ClientCredentials).to receive(:get_token).and_return(Token.new('s', 0))
          expect(client).to receive(:sleep).exactly(2).times
          expect{ client.oauth_token(num_retry: 2) }.to raise_error(Sand::ExpiredTokenError)
        end
      end
    end

    class Token
      attr_accessor :token, :expires_in
      def initialize(token, exp)
        @token = token
        @expires_in = exp
      end
    end
  end

  describe '#status_code' do
    it 'returns nil when given nil' do
      expect(client.send(:status_code, nil)).to be_nil
    end
  end
end
