require 'unit_spec_helper'

describe Rpush::Daemon::Adm::Delivery do
  let(:app) { Rpush::Adm::App.new(:name => 'MyApp', :client_id => 'CLIENT_ID', :client_secret => 'CLIENT_SECRET') }
  let(:notification) { Rpush::Adm::Notification.create!(:app => app, :registration_ids => ['xyz'], :deliver_after => Time.now, :data => {'message' => 'test'}) }
  let(:logger) { double(:error => nil, :info => nil, :warn => nil) }
  let(:response) { double(:code => 200, :header => {}) }
  let(:http) { double(:shutdown => nil, :request => response)}
  let(:now) { Time.parse('2012-10-14 00:00:00') }
  let(:batch) { double(:mark_failed => nil, :mark_delivered => nil, :mark_retryable => nil) }
  let(:delivery) { Rpush::Daemon::Adm::Delivery.new(app, http, notification, batch) }
  let(:store) { double(:create_adm_notification => double(:id => 2)) }

  def perform
    delivery.perform
  end

  before do
    app.access_token = 'ACCESS_TOKEN'
    app.access_token_expiration = Time.now + 1.month

    delivery.stub(:reflect => nil)
    Rpush::Daemon.stub(:store => store)
    Time.stub(:now => now)
    Rpush.stub(:logger => logger)
  end

  describe 'unknown error response' do
    before do
      response.stub(:code => 408)
    end

    it 'marks the notification as failed because no successful delivery was made' do
      response.stub(:body => JSON.dump({ 'reason' => 'InvalidData' }))
      delivery.should_receive(:mark_failed).with(408, 'Request Timeout')
      expect { perform }.to raise_error(Rpush::DeliveryError)
    end
  end

  describe 'a 200 (Ok) response' do
    before do
      response.stub(:code => 200)
    end

    it 'marks the notification as delivered if delivered successfully to all devices' do
      response.stub(:body => JSON.dump({ 'registrationID' => 'xyz' }))
      delivery.should_receive(:mark_delivered)
      perform
    end

    it 'logs that the notification was delivered' do
      response.stub(:body => JSON.dump({ 'registrationID' => 'xyz' }))
      logger.should_receive(:info).with("[MyApp] #{notification.id} sent to xyz")
      perform
    end

    it 'reflects on canonical IDs' do
      response.stub(:body => JSON.dump({'registrationID' => 'canonical123' }))
      notification.stub(:registration_ids => ['1'])
      delivery.should_receive(:reflect).with(:adm_canonical_id, '1', 'canonical123')
      perform
    end
  end

  describe 'a 400 (Bad Request) response' do
    before do
      response.stub(:code => 400)
    end

    it 'marks the notification as failed because no successful delivery was made' do
      response.stub(:body => JSON.dump({ 'reason' => 'InvalidData' }))
      delivery.should_receive(:mark_failed).with(nil, 'Failed to deliver to all recipients.')
      expect { perform }.to raise_error(Rpush::DeliveryError)
    end

    it 'logs that the notification was not delivered' do
      response.stub(:body => JSON.dump({ 'reason' => 'InvalidRegistrationId' }))
      logger.should_receive(:warn).with("[MyApp] bad_request: xyz (InvalidRegistrationId)")
      expect { perform }.to raise_error(Rpush::DeliveryError)
    end
  end

  describe 'a 401 (Unauthorized) response' do
    let(:http) { double(:shutdown => nil)}
    let(:token_response) { double(:code => 200, :header => {}, :body => JSON.dump({'access_token' => 'ACCESS_TOKEN', 'expires_in' => 60})) }

    before do
      response.stub(:code => 401, :header => { 'retry-after' => 10 })

      # first request to deliver message that returns unauthorized response
      adm_uri = URI.parse(Rpush::Daemon::Adm::Delivery::AMAZON_ADM_URL % [notification.registration_ids.first])
      http.should_receive(:request).with(adm_uri, instance_of(Net::HTTP::Post)).and_return(response)
    end

    it 'should retrieve a new access token and mark the notification for retry' do
      # request for access token
      http.should_receive(:request).with(Rpush::Daemon::Adm::Delivery::AMAZON_TOKEN_URI, instance_of(Net::HTTP::Post)).and_return(token_response)

      store.should_receive(:update_app).with(notification.app)
      delivery.should_receive(:mark_retryable).with(notification, now)

      perform
    end

    it 'should update the app with the new access token' do
      # request for access token
      http.should_receive(:request).with(Rpush::Daemon::Adm::Delivery::AMAZON_TOKEN_URI, instance_of(Net::HTTP::Post)).and_return(token_response)

      store.should_receive(:update_app).with do |app|
        app.access_token.should eq 'ACCESS_TOKEN'
        app.access_token_expiration.should eq now + 60.seconds
      end
      delivery.should_receive(:mark_retryable).with(notification, now)

      perform
    end

    it 'should log the error and stop retrying if new access token can\'t be retrieved' do
      token_response.stub(:code => 404, :body => "test")
      # request for access token
      http.should_receive(:request).with(Rpush::Daemon::Adm::Delivery::AMAZON_TOKEN_URI, instance_of(Net::HTTP::Post)).and_return(token_response)

      store.should_not_receive(:update_app).with(notification.app)
      delivery.should_not_receive(:mark_retryable)

      logger.should_receive(:warn).with("[MyApp] Could not retrieve access token from ADM: test")

      perform
    end
  end

  describe 'a 429 (Too Many Request) response' do
    let(:http) { double(:shutdown => nil) }
    let(:notification) { Rpush::Adm::Notification.create!(:app => app, :registration_ids => ['abc','xyz'], :deliver_after => Time.now, :collapse_key => 'sync', :data => {'message' => 'test'}) }
    let(:too_many_request_response) { double(:code => 429, :header => { 'retry-after' => 3600 }) }

    it 'should retry the entire notification respecting the Retry-After header if none sent out yet' do
      response.stub(:code => 429, :header => { 'retry-after' => 3600 })

      # first request to deliver message that returns too many request response
      adm_uri = URI.parse(Rpush::Daemon::Adm::Delivery::AMAZON_ADM_URL % [notification.registration_ids.first])
      http.should_receive(:request).with(adm_uri, instance_of(Net::HTTP::Post)).and_return(response)

      delivery.should_receive(:mark_retryable).with(notification, now + 1.hour)
      perform
    end

    it 'should retry the entire notification using exponential backoff' do
      response.stub(:code => 429, :header => {})

      # first request to deliver message that returns too many request response
      adm_uri = URI.parse(Rpush::Daemon::Adm::Delivery::AMAZON_ADM_URL % [notification.registration_ids.first])
      http.should_receive(:request).with(adm_uri, instance_of(Net::HTTP::Post)).and_return(response)

      delivery.should_receive(:mark_retryable).with(notification, Time.now + 2 ** (notification.retries + 1))
      perform
    end

    it 'should keep sent reg ids in original notification and create new notification with remaining reg ids for retry' do
      response.stub(:code => 200, :body => JSON.dump({ 'registrationID' => 'abc' }))

      # first request to deliver message succeeds
      adm_uri = URI.parse(Rpush::Daemon::Adm::Delivery::AMAZON_ADM_URL % ['abc'])
      http.should_receive(:request).with(adm_uri, instance_of(Net::HTTP::Post)).and_return(response)

      # first request to deliver message that returns too many request response
      adm_uri = URI.parse(Rpush::Daemon::Adm::Delivery::AMAZON_ADM_URL % ['xyz'])
      http.should_receive(:request).with(adm_uri, instance_of(Net::HTTP::Post)).and_return(too_many_request_response)

      store.should_receive(:update_notification).with do |notif|
        notif.registration_ids.include?('abc').should be_true
        notif.registration_ids.include?('xyz').should be_false
      end

      store.should_receive(:create_adm_notification).with do |attrs, notification_data, reg_ids, deliver_after, notification_app|
        attrs.has_key?('collapse_key').should be_true
        attrs.has_key?('delay_while_idle').should be_true
        attrs.has_key?('app_id').should be_true

        reg_ids.should eq ['xyz']
        deliver_after.should eq now + 1.hour
        notification_app.should eq notification.app
      end

      delivery.should_receive(:mark_delivered)

      perform
    end
  end

  describe 'a 500 (Internal Server Error) response' do
    before do
      response.stub(:code => 500)
    end

    it 'marks the notification as failed because no successful delivery was made' do
      delivery.should_receive(:mark_failed).with(nil, 'Failed to deliver to all recipients.')
      expect { perform }.to raise_error(Rpush::DeliveryError)
    end

    it 'logs that the notification was not delivered' do
      logger.should_receive(:warn).with("[MyApp] internal_server_error: xyz (Internal Server Error)")
      expect { perform }.to raise_error(Rpush::DeliveryError)
    end
  end

  describe 'a 503 (Service Unavailable) response' do
    before do
      response.stub(:code => 503, :header => { 'retry-after' => 10 })
    end

    it 'should retry the notification respecting the Retry-After header' do
      delivery.should_receive(:mark_retryable).with(notification, now + 10.seconds)
      perform
    end
  end

  describe 'some registration ids succeeding and some failing' do
    let(:http) { double(:shutdown => nil) }
    let(:notification) { Rpush::Adm::Notification.create!(:app => app, :registration_ids => ['abc','xyz'], :deliver_after => Time.now, :collapse_key => 'sync', :data => {'message' => 'test'}) }
    let(:bad_request_response) { double(:code => 400, :body => JSON.dump({ 'reason' => 'InvalidData' })) }

    it 'should keep sent reg ids in original notification and create new notification with remaining reg ids for retry' do
      response.stub(:code => 200, :body => JSON.dump({ 'registrationID' => 'abc' }))

      # first request to deliver message succeeds
      adm_uri = URI.parse(Rpush::Daemon::Adm::Delivery::AMAZON_ADM_URL % ['abc'])
      http.should_receive(:request).with(adm_uri, instance_of(Net::HTTP::Post)).and_return(response)

      # first request to deliver message that returns too many request response
      adm_uri = URI.parse(Rpush::Daemon::Adm::Delivery::AMAZON_ADM_URL % ['xyz'])
      http.should_receive(:request).with(adm_uri, instance_of(Net::HTTP::Post)).and_return(bad_request_response)

      store.should_receive(:update_notification).with do |notif|
        notif.error_description.should eq "Failed to deliver to recipients: \nxyz: InvalidData"
      end

      delivery.should_receive(:mark_delivered)

      perform
    end
  end
end
