require 'unit_spec_helper'

describe 'Shoulda::Matchers::ActionController::RouteMatcher', type: :controller do
  shared_examples_for 'tests involving expected route parts' do |args|
    include_controller_in_expected_route_options =
      args.fetch(:include_controller_in_expected_route_options)

    context 'when all parts of the expected route match an existing route' do
      it 'accepts' do
        define_route :get, '/', action: 'index'

        assert_accepts add_target_to(
          route(:get, '/'),
          build_expected_route_options(
            include_controller_in_expected_route_options,
            action: 'index',
          ),
        )
      end

      if include_controller_in_expected_route_options
        context 'and the expected controller is specified as a symbol' do
          it 'accepts' do
            define_route :get, '/', action: 'index'

            assert_accepts add_target_to(
              route(:get, '/'),
              build_expected_route_options(
                include_controller_in_expected_route_options,
                action: 'index',
              ),
            )
          end
        end
      end

      context 'and the expected action is specified as a symbol' do
        it 'accepts' do
          define_route :get, '/', action: 'index'

          assert_accepts add_target_to(
            route(:get, '/'),
            build_expected_route_options(
              include_controller_in_expected_route_options,
              action: :index,
            ),
          )
        end
      end
    end

    context 'when no parts of the expected route match an existing route' do
      it 'rejects' do
        assert_rejects add_target_to(
          route(:get, '/non_existent_route'),
          controller: 'no_controller',
          action: 'no_action',
        )
      end
    end

    context 'when all parts of the expected route but the method match an existing route' do
      it 'rejects' do
        define_route :post, '/', action: 'index'

        assert_rejects add_target_to(
          route(:get, '/'),
          build_expected_route_options(
            include_controller_in_expected_route_options,
            action: 'index',
          ),
        )
      end
    end

    context 'when all parts of the expected route but the path match an existing route' do
      it 'rejects' do
        define_route :get, '/', action: 'index'

        assert_rejects add_target_to(
          route(:get, '/different_path'),
          build_expected_route_options(
            include_controller_in_expected_route_options,
            action: 'index',
          ),
        )
      end
    end

    if include_controller_in_expected_route_options
      context 'when all parts of the expected route but the controller match an existing route' do
        it 'rejects' do
          define_route :get, '/', controller: 'another_controller', action: 'index'

          assert_rejects add_target_to(
            route(:get, '/'),
            build_expected_route_options(
              include_controller_in_expected_route_options,
              action: 'index',
            ),
          )
        end
      end
    end

    context 'when all parts of the expected route but the action match an existing route' do
      it 'rejects' do
        define_route :get, '/', action: 'index'

        assert_rejects add_target_to(
          route(:get, '/'),
          build_expected_route_options(
            include_controller_in_expected_route_options,
            action: 'another_action',
          ),
        )
      end
    end
  end

  shared_examples_for 'tests involving params' do
    context 'when the actual route has a param' do
      context 'and the expected params include that param' do
        it 'accepts' do
          define_route :get, "/#{controller_name}/:id", action: 'show'

          assert_accepts add_target_to(
            route(:get, "/#{controller_name}/1"),
            controller: controller_name,
            action: 'show',
            id: '1',
          )
        end

        context 'but its value was not specified as a string' do
          it 'accepts, treating it as a string' do
            define_route :get, "/#{controller_name}/:id", action: 'show'

            assert_accepts add_target_to(
              route(:get, "/#{controller_name}/1"),
              controller: controller_name,
              action: 'show',
              id: 1,
            )
          end
        end
      end

      context 'and the expected params do not match the actual params' do
        it 'rejects' do
          define_route :get, "/#{controller_name}/:id", action: 'show'

          params = {
            controller: controller_name,
            action: 'show',
            some: 'other',
            params: 'here',
          }
          assert_rejects add_target_to(
            route(:get, "/#{controller_name}/:id"),
            params,
          )
        end
      end
    end

    context 'when the actual route has a default param whose value is a symbol' do
      context 'and the expected params include a value for it' do
        context 'as a symbol' do
          it 'accepts' do
            define_route :post, "/#{controller_name}/(.:format)",
              action: 'create',
              defaults: { format: :json }

            assert_accepts add_target_to(
              route(:post, "/#{controller_name}"),
              controller: controller_name,
              action: 'create',
              format: :json,
            )
          end
        end

        context 'as a string' do
          it 'accepts' do
            define_route :post, "/#{controller_name}/(.:format)",
              action: 'create',
              defaults: { format: :json }

            assert_accepts add_target_to(
              route(:post, "/#{controller_name}"),
              controller: controller_name,
              action: 'create',
              format: 'json',
            )
          end
        end
      end
    end

    context 'when the existing route has a glob segment' do
      context 'and a param is given which represents the segment' do
        it 'accepts' do
          define_route :get, "/#{controller_name}/*id", action: 'whatever'

          assert_accepts add_target_to(
            route(:get, "/#{controller_name}/foo/bar"),
            controller: controller_name,
            action: 'whatever',
            id: 'foo/bar',
          )
        end
      end

      context 'and no param is given which represents the segment' do
        it 'rejects' do
          define_route :get, "/#{controller_name}/*id", action: 'whatever'

          assert_rejects add_target_to(
            route(:get, "/#{controller_name}"),
            controller: controller_name,
            action: 'whatever',
          )
        end
      end
    end
  end

  shared_examples_for 'core tests' do
    context 'given a controller and action specified as individual options' do
      include_examples 'tests involving expected route parts',
        include_controller_in_expected_route_options: true

      include_examples 'tests involving params'

      def add_target_to(route_matcher, params)
        route_matcher.to(params)
      end
    end

    context 'given a controller and action joined together in a string' do
      include_examples 'tests involving expected route parts',
        include_controller_in_expected_route_options: true

      include_examples 'tests involving params'

      def add_target_to(route_matcher, args)
        controller = args.fetch(:controller)
        action = args.fetch(:action)
        route_matcher.to("#{controller}##{action}", args)
      end
    end

    context 'given just an action' do
      include_examples 'tests involving expected route parts',
        include_controller_in_expected_route_options: false

      include_examples 'tests involving params'

      def add_target_to(route_matcher, params)
        route_matcher.to(params)
      end
    end
  end

  before do
    setup_rails_controller_test(controller_class)
  end

  context 'given a controller that is not namespaced' do
    include_examples 'core tests'

    def controller_class_name
      'ExamplesController'
    end
  end

  context 'given a controller that is namespaced' do
    def define_controller_under_test
      define_module('Admin')
      super
    end

    include_examples 'core tests'

    def controller_class_name
      'Admin::ExamplesController'
    end
  end

  let(:controller_class) do
    define_controller_under_test
  end

  def define_controller_under_test
    define_controller(controller_class_name)
  end

  def controller_name
    controller_class_name.sub(/Controller$/, '').underscore
  end

  def define_route(method, path, args)
    action = args.fetch(:action)
    controller = args.fetch(:controller) { controller_name }
    define_routes do
      public_send(
        method,
        path,
        args.merge(controller:, action:),
      )
    end
  end

  def build_expected_route_options(include_controller_in_expected_route_options, default_options)
    default_options.dup.tap do |options|
      if include_controller_in_expected_route_options
        options[:controller] = controller_name
      end
    end
  end

  def assert_accepts(matcher)
    expect(controller).to matcher
  end

  def assert_rejects(matcher)
    expect(controller).not_to matcher
  end
end
