require 'spec_helper'

describe Freddy do
  let(:freddy) { described_class.build(logger, config) }

  let(:destination)  { random_destination }
  let(:destination2) { random_destination }
  let(:payload)      { {pay: 'load'} }

  after { freddy.close }

  def respond_to(&block)
    freddy.respond_to(destination, &block)
  end

  context 'when making a send-and-forget request' do
    context 'with timeout' do
      it 'removes the message from the queue after the timeout' do
        # Assume that there already is a queue. Otherwise will get an early
        # return.
        freddy.channel.queue(destination)

        freddy.deliver(destination, {}, timeout: 0.1)
        sleep 0.2

        processed_after_timeout = false
        respond_to { processed_after_timeout = true }
        default_sleep

        expect(processed_after_timeout).to be(false)
      end
    end

    context 'without timeout' do
      it 'keeps the message in the queue' do
        # Assume that there already is a queue. Otherwise will get an early
        # return.
        freddy.channel.queue(destination)

        freddy.deliver(destination, {})
        default_sleep # to ensure everything is properly cleaned

        processed_after_timeout = false
        respond_to { processed_after_timeout = true }
        default_sleep

        expect(processed_after_timeout).to be(true)
      end
    end
  end

  context 'when making a synchronized request' do
    it 'returns response as soon as possible' do
      respond_to { |payload, msg_handler| msg_handler.success(res: 'yey') }
      response = freddy.deliver_with_response(destination, {a: 'b'})

      expect(response).to eq(res: 'yey')
    end

    it 'raises an error if the message was errored' do
      respond_to { |payload, msg_handler| msg_handler.error(error: 'not today') }

      expect {
        freddy.deliver_with_response(destination, payload)
      }.to raise_error(Freddy::InvalidRequestError) {|error|
        expect(error.response).to eq(error: 'not today')
      }
    end

    it 'does not leak consumers' do
      respond_to { |payload, msg_handler| msg_handler.success(res: 'yey') }

      old_count = freddy.channel.consumers.keys.count

      response1 = freddy.deliver_with_response(destination, {a: 'b'})
      response2 = freddy.deliver_with_response(destination, {a: 'b'})

      expect(response1).to eq(res: 'yey')
      expect(response2).to eq(res: 'yey')

      new_count = freddy.channel.consumers.keys.count
      expect(new_count).to be(old_count + 1)
    end

    it 'responds to the correct requester' do
      respond_to { |payload, msg_handler| msg_handler.success(res: 'yey') }

      response = freddy.deliver_with_response(destination, payload)
      expect(response).to eq(res: 'yey')

      expect {
        freddy.deliver_with_response(destination2, payload)
      }.to raise_error(Freddy::InvalidRequestError)
    end

    context 'when queue does not exist' do
      it 'gives a no route error' do
        begin
          Timeout::timeout(0.5) do
            expect {
              freddy.deliver_with_response(destination, {a: 'b'}, timeout: 3)
            }.to raise_error(Freddy::InvalidRequestError) {|error|
              expect(error.response).to eq(error: 'Specified queue does not exist')
            }
          end
        rescue Timeout::Error
          fail('Received a timeout error instead of the no route error')
        end
      end
    end

    context 'on timeout' do
      it 'gives timeout error' do
        respond_to { |payload, msg_handler| sleep 0.2 }

        expect {
          freddy.deliver_with_response(destination, {a: 'b'}, timeout: 0.1)
        }.to raise_error(Freddy::TimeoutError) {|error|
          expect(error.response).to eq(error: 'RequestTimeout', message: 'Timed out waiting for response')
        }
      end

      context 'with delete_on_timeout is set to true' do
        it 'removes the message from the queue' do
          # Assume that there already is a queue. Otherwise will get an early
          # return.
          freddy.channel.queue(destination)

          expect {
            freddy.deliver_with_response(destination, {}, timeout: 0.1)
          }.to raise_error(Freddy::TimeoutError)
          default_sleep # to ensure everything is properly cleaned

          processed_after_timeout = false
          respond_to { processed_after_timeout = true }
          default_sleep

          expect(processed_after_timeout).to be(false)
        end
      end

      context 'with delete_on_timeout is set to false' do
        it 'removes the message from the queue' do
          # Assume that there already is a queue. Otherwise will get an early
          # return.
          freddy.channel.queue(destination)

          expect {
            freddy.deliver_with_response(destination, {}, timeout: 0.1, delete_on_timeout: false)
          }.to raise_error(Freddy::TimeoutError)
          default_sleep # to ensure everything is properly cleaned

          processed_after_timeout = false
          respond_to { processed_after_timeout = true }
          default_sleep

          expect(processed_after_timeout).to be(true)
        end
      end
    end
  end

  describe 'when tapping' do
    def tap(custom_destination = destination, &block)
      freddy.tap_into(custom_destination, &block)
    end

    it 'receives messages' do
      tap {|msg| @tapped_message = msg }
      deliver

      wait_for { @tapped_message }
      expect(@tapped_message).to eq(payload)
    end

    it 'has the destination' do
      tap "somebody.*.love" do |message, destination|
        @destination = destination
      end
      deliver "somebody.to.love"

      wait_for { @destination }
      expect(@destination).to eq("somebody.to.love")
    end

    it "doesn't consume the message" do
      tap { @tapped = true }
      respond_to { @message_received = true }

      deliver

      wait_for { @tapped }
      wait_for { @message_received }
      expect(@tapped).to be(true)
      expect(@message_received).to be(true)
    end

    it "allows * wildcard" do
      tap("somebody.*.love") { @tapped = true }

      deliver "somebody.to.love"

      wait_for { @tapped }
      expect(@tapped).to be(true)
    end

    it "* matches only one word" do
      tap("somebody.*.love") { @tapped = true }

      deliver "somebody.not.to.love"

      default_sleep
      expect(@tapped).to be_falsy
    end

    it "allows # wildcard" do
      tap("i.#.free") { @tapped = true }

      deliver "i.want.to.break.free"

      wait_for { @tapped }
      expect(@tapped).to be(true)
    end
  end
end