module RSpec::Rails::Matchers
  module RoutingMatchers
    extend RSpec::Matchers::DSL

    class RouteToMatcher < RSpec::Matchers::BuiltIn::BaseMatcher

      def initialize(scope, *expected)
        @scope = scope
        @expected = expected[1] || {}
        if Hash === expected[0]
          @expected.merge!(expected[0])
        else
          controller, action = expected[0].split('#')
          @expected.merge!(:controller => controller, :action => action)
        end
      end

      # @api private
      def matches?(verb_to_path_map)
        @actual = @verb_to_path_map = verb_to_path_map
        # assert_recognizes does not consider ActionController::RoutingError an
        # assertion failure, so we have to capture that and Assertion here.
        match_unless_raises ActiveSupport::TestCase::Assertion, ActionController::RoutingError do
          path, query = *verb_to_path_map.values.first.split('?')
          @scope.assert_recognizes(
            @expected,
            {:method => verb_to_path_map.keys.first, :path => path},
            Rack::Utils::parse_query(query)
          )
        end
      end

      # @api private
      def failure_message_for_should
        rescued_exception.message
      end

      def description
        "route #{@actual.inspect} to #{@expected.inspect}"
      end
    end

    # Delegates to `assert_recognizes`. Supports short-hand controller/action
    # declarations (e.g. `"controller#action"`).
    #
    # @example
    #
    #     expect(:get => "/things/special").to route_to(
    #       :controller => "things",
    #       :action     => "special"
    #     )
    #
    #     expect(:get => "/things/special").to route_to("things#special")
    #
    # @see http://api.rubyonrails.org/classes/ActionDispatch/Assertions/RoutingAssertions.html#method-i-assert_recognizes
    def route_to(*expected)
      RouteToMatcher.new(self, *expected)
    end

    class BeRoutableMatcher < RSpec::Matchers::BuiltIn::BaseMatcher

      def initialize(scope)
        @scope = scope
      end

      # @api private
      def matches?(path)
        @actual = path
        match_unless_raises ActionController::RoutingError do
          @routing_options = @scope.routes.recognize_path(
            path.values.first, :method => path.keys.first
          )
        end
      end

      def failure_message_for_should
        "expected #{@actual.inspect} to be routable"
      end

      def failure_message_for_should_not
        "expected #{@actual.inspect} not to be routable, but it routes to #{@routing_options.inspect}"
      end
    end

    # Passes if the route expression is recognized by the Rails router based on
    # the declarations in `config/routes.rb`. Delegates to
    # `RouteSet#recognize_path`.
    #
    # @example
    #
    # You can use route helpers provided by rspec-rails.
    #     expect(:get  => "/a/path").to be_routable
    #     expect(:post => "/another/path").to be_routable
    #     expect(:put  => "/yet/another/path").to be_routable
    def be_routable
      BeRoutableMatcher.new(self)
    end

    module RouteHelpers
      %w(get post put patch delete options head).each do |method|
        define_method method do |path|
          { method.to_sym => path }
        end
      end
    end
  end
end
