module Shoulda
  module Matchers
    module ActionController
      # The `respond_with` matcher tests that an action responds with a certain
      # status code.
      #
      # You can specify that the status should be a number:
      #
      #     class PostsController < ApplicationController
      #       def index
      #         render status: 403
      #       end
      #     end
      #
      #     # RSpec
      #     RSpec.describe PostsController, type: :controller do
      #       describe 'GET #index' do
      #         before { get :index }
      #
      #         it { should respond_with(403) }
      #       end
      #     end
      #
      #     # Minitest (Shoulda)
      #     class PostsControllerTest < ActionController::TestCase
      #       context 'GET #index' do
      #         setup { get :index }
      #
      #         should respond_with(403)
      #       end
      #     end
      #
      # You can specify that the status should be within a range of numbers:
      #
      #     class PostsController < ApplicationController
      #       def destroy
      #         render status: 508
      #       end
      #     end
      #
      #     # RSpec
      #     RSpec.describe PostsController, type: :controller do
      #       describe 'DELETE #destroy' do
      #         before { delete :destroy }
      #
      #         it { should respond_with(500..600) }
      #       end
      #     end
      #
      #     # Minitest (Shoulda)
      #     class PostsControllerTest < ActionController::TestCase
      #       context 'DELETE #destroy' do
      #         setup { delete :destroy }
      #
      #         should respond_with(500..600)
      #       end
      #     end
      #
      # Finally, you can specify that the status should be a symbol:
      #
      #     class PostsController < ApplicationController
      #       def show
      #         render status: :locked
      #       end
      #     end
      #
      #     # RSpec
      #     RSpec.describe PostsController, type: :controller do
      #       describe 'GET #show' do
      #         before { get :show }
      #
      #         it { should respond_with(:locked) }
      #       end
      #     end
      #
      #     # Minitest (Shoulda)
      #     class PostsControllerTest < ActionController::TestCase
      #       context 'GET #show' do
      #         setup { get :show }
      #
      #         should respond_with(:locked)
      #       end
      #     end
      #
      # @return [RespondWithMatcher]
      #
      def respond_with(status)
        RespondWithMatcher.new(status)
      end

      # @private
      class RespondWithMatcher
        def initialize(status)
          @status = symbol_to_status_code(status)
        end

        def matches?(controller)
          @controller = controller
          correct_status_code? || correct_status_code_range?
        end

        def failure_message
          "Expected #{expectation}"
        end

        def failure_message_when_negated
          "Did not expect #{expectation}"
        end

        def description
          "respond with #{@status}"
        end

        protected

        def correct_status_code?
          response_code == @status
        end

        def correct_status_code_range?
          @status.is_a?(Range) &&
            @status.include?(response_code)
        end

        def response_code
          @controller.response.response_code
        end

        def symbol_to_status_code(potential_symbol)
          case potential_symbol
          when :success  then 200
          when :redirect then 300..399
          when :missing  then 404
          when :error    then 500..599
          when Symbol
            ::Rack::Utils.status_code(potential_symbol)
          else
            potential_symbol
          end
        end

        def expectation
          "response to be a #{@status}, but was #{response_code}"
        end
      end
    end
  end
end
