# frozen_string_literal: true

require "abstract_unit"
require "rack/utils"

module ActionDispatch
  module Journey
    class TestRouter < ActiveSupport::TestCase
      attr_reader :mapper, :routes, :route_set, :router

      def setup
        @app = Routing::RouteSet::Dispatcher.new({})
        @route_set = ActionDispatch::Routing::RouteSet.new
        @routes = @route_set.router.routes
        @router = @route_set.router
        @formatter = @route_set.formatter
        @mapper = ActionDispatch::Routing::Mapper.new @route_set
      end

      def test_dashes
        get "/foo-bar-baz", to: "foo#bar"

        env = rails_env "PATH_INFO" => "/foo-bar-baz"
        called = false
        router.recognize(env) do |r, params|
          called = true
        end
        assert called
      end

      def test_unicode
        get "/ほげ", to: "foo#bar"

        # match the escaped version of /ほげ
        env = rails_env "PATH_INFO" => "/%E3%81%BB%E3%81%92"
        called = false
        router.recognize(env) do |r, params|
          called = true
        end
        assert called
      end

      def test_regexp_first_precedence
        get "/whois/:domain", domain: /\w+\.[\w\.]+/, to: "foo#bar"
        get "/whois/:id(.:format)", to: "foo#baz"

        env = rails_env "PATH_INFO" => "/whois/example.com"

        list = []
        router.recognize(env) do |r, params|
          list << r
        end
        assert_equal 2, list.length

        r = list.first

        assert_equal "/whois/:domain(.:format)", r.path.spec.to_s
      end

      def test_required_parts_verified_are_anchored
        get "/foo/:id", id: /\d/, anchor: false, to: "foo#bar"

        assert_raises(ActionController::UrlGenerationError) do
          @route_set.url_for({ controller: "foo", action: "bar", id: "10" }, nil)
        end
      end

      def test_required_parts_are_verified_when_building
        get "/foo/:id", id: /\d+/, anchor: false, to: "foo#bar"

        path, _ = _generate(nil, { controller: "foo", action: "bar", id: "10" }, {})
        assert_equal "/foo/10", path

        assert_raises(ActionController::UrlGenerationError) do
          _generate(nil, { id: "aa" }, {})
        end
      end

      def test_only_required_parts_are_verified
        get "/foo(/:id)", id: /\d/, to: "foo#bar"

        path, _ = _generate(nil, { controller: "foo", action: "bar", id: "10" }, {})
        assert_equal "/foo/10", path

        path, _ = _generate(nil, { controller: "foo", action: "bar" }, {})
        assert_equal "/foo", path

        path, _ = _generate(nil, { controller: "foo", action: "bar", id: "aa" }, {})
        assert_equal "/foo/aa", path
      end

      def test_knows_what_parts_are_missing_from_named_route
        route_name = "gorby_thunderhorse"
        get "/foo/:id", as: route_name, id: /\d+/, to: "foo#bar"

        error = assert_raises(ActionController::UrlGenerationError) do
          _generate(route_name, {}, {})
        end

        assert_match(/missing required keys: \[:id\]/, error.message)
      end

      def test_does_not_include_missing_keys_message
        route_name = "gorby_thunderhorse"

        error = assert_raises(ActionController::UrlGenerationError) do
          _generate(route_name, {}, {})
        end

        assert_no_match(/missing required keys: \[\]/, error.message)
      end

      def test_X_Cascade
        get "/messages(.:format)", to: "foo#bar"
        resp = router.serve(rails_env("REQUEST_METHOD" => "GET", "PATH_INFO" => "/lol"))
        assert_equal ["Not Found"], resp.last
        assert_equal "pass", resp[1]["X-Cascade"]
        assert_equal 404, resp.first
      end

      def test_clear_trailing_slash_from_script_name_on_root_unanchored_routes
        app = lambda { |env| [200, {}, ["success!"]] }
        get "/weblog", to: app

        env  = rack_env("SCRIPT_NAME" => "", "PATH_INFO" => "/weblog")
        resp = route_set.call env
        assert_equal ["success!"], resp.last
        assert_equal "", env["SCRIPT_NAME"]
      end

      def test_defaults_merge_correctly
        get "/foo(/:id)", to: "foo#bar", id: nil

        env = rails_env "PATH_INFO" => "/foo/10"
        router.recognize(env) do |r, params|
          assert_equal({ id: "10", controller: "foo", action: "bar" }, params)
        end

        env = rails_env "PATH_INFO" => "/foo"
        router.recognize(env) do |r, params|
          assert_equal({ id: nil, controller: "foo", action: "bar" }, params)
        end
      end

      def test_recognize_with_unbound_regexp
        get "/foo", anchor: false, to: "foo#bar"

        env = rails_env "PATH_INFO" => "/foo/bar"

        router.recognize(env) { |*_| }

        assert_equal "/foo", env.env["SCRIPT_NAME"]
        assert_equal "/bar", env.env["PATH_INFO"]
      end

      def test_bound_regexp_keeps_path_info
        get "/foo", to: "foo#bar"

        env = rails_env "PATH_INFO" => "/foo"

        before = env.env["SCRIPT_NAME"]

        router.recognize(env) { |*_| }

        assert_equal before, env.env["SCRIPT_NAME"]
        assert_equal "/foo", env.env["PATH_INFO"]
      end

      def test_path_not_found
        [
          "/messages(.:format)",
          "/messages/new(.:format)",
          "/messages/:id/edit(.:format)",
          "/messages/:id(.:format)"
        ].each do |path|
          get path, to: "foo#bar"
        end
        env = rails_env "PATH_INFO" => "/messages/unknown/path"
        yielded = false

        router.recognize(env) do |*whatever|
          yielded = true
        end
        assert_not yielded
      end

      def test_required_part_in_recall
        get "/messages/:a/:b", to: "foo#bar"

        path, _ = _generate(nil, { controller: "foo", action: "bar", a: "a" }, { b: "b" })
        assert_equal "/messages/a/b", path
      end

      def test_splat_in_recall
        get "/*path", to: "foo#bar"

        path, _ = _generate(nil, { controller: "foo", action: "bar" }, { path: "b" })
        assert_equal "/b", path
      end

      def test_recall_should_be_used_when_scoring
        get "/messages/:action(/:id(.:format))", to: "foo#bar"
        get "/messages/:id(.:format)", to: "bar#baz"

        path, _ = _generate(nil, { controller: "foo", id: 10 }, { action: "index" })
        assert_equal "/messages/index/10", path
      end

      def test_nil_path_parts_are_ignored
        get "/:controller(/:action(.:format))", to: "tasks#lol"

        params = { controller: "tasks", format: nil }
        extras = { action: "lol" }

        path, _ = _generate(nil, params, extras)
        assert_equal "/tasks/index", path
      end

      def test_generate_slash
        params = [ [:controller, "tasks"],
                   [:action, "show"] ]
        get "/", Hash[params]

        path, _ = _generate(nil, Hash[params], {})
        assert_equal "/", path
      end

      def test_generate_id
        get "/:controller(/:action)", to: "foo#bar"

        path, params = _generate(
          nil, { id: 1, controller: "tasks", action: "show" }, {})
        assert_equal "/tasks/show", path
        assert_equal({ id: "1" }, params)
      end

      def test_generate_escapes
        get "/:controller(/:action)", to: "foo#bar"

        path, _ = _generate(nil,
          { controller: "tasks",
                 action: "a/b c+d",
        }, {})
        assert_equal "/tasks/a%2Fb%20c+d", path
      end

      def test_generate_escapes_with_namespaced_controller
        get "/:controller(/:action)", to: "foo#bar"

        path, _ = _generate(
          nil, { controller: "admin/tasks",
                 action: "a/b c+d",
        }, {})
        assert_equal "/admin/tasks/a%2Fb%20c+d", path
      end

      def test_generate_extra_params
        get "/:controller(/:action)", to: "foo#bar"

        path, params = _generate(
          nil, { id: 1,
                 controller: "tasks",
                 action: "show",
                 relative_url_root: nil
        }, {})
        assert_equal "/tasks/show", path
        assert_equal({ id: "1" }, params)
      end

      def test_generate_missing_keys_no_matches_different_format_keys
        get "/:controller/:action/:name", to: "foo#bar"
        primary_parameters = {
          id: 1,
          controller: "tasks",
          action: "show",
          relative_url_root: nil
        }
        redirection_parameters = {
          "action" => "show",
        }
        missing_key = "name"
        missing_parameters = {
          missing_key => "task_1"
        }
        request_parameters = primary_parameters.merge(redirection_parameters).merge(missing_parameters)

        message = "No route matches #{Hash[request_parameters.sort_by { |k, _|k.to_s }].inspect}, missing required keys: #{[missing_key.to_sym].inspect}"

        error = assert_raises(ActionController::UrlGenerationError) do
          _generate(
            nil, request_parameters, request_parameters)
        end
        assert_equal message, error.message
      end

      def test_generate_uses_recall_if_needed
        get "/:controller(/:action(/:id))", to: "foo#bar"

        path, params = _generate(
          nil,
          { controller: "tasks", id: 10 },
          { action: "index" })
        assert_equal "/tasks/index/10", path
        assert_equal({}, params)
      end

      def test_generate_with_name
        get "/:controller(/:action)", to: "foo#bar", as: "tasks"

        path, params = _generate(
          "tasks",
          { controller: "tasks" },
          { controller: "tasks", action: "index" })
        assert_equal "/tasks/index", path
        assert_equal({}, params)
      end

      {
        "/content"          => { controller: "content" },
        "/content/list"     => { controller: "content", action: "list" },
        "/content/show/10"  => { controller: "content", action: "show", id: "10" },
      }.each do |request_path, expected|
        define_method("test_recognize_#{expected.keys.map(&:to_s).join('_')}") do
          get "/:controller(/:action(/:id))", to: "foo#bar"
          route = @routes.first

          env = rails_env "PATH_INFO" => request_path
          called = false

          router.recognize(env) do |r, params|
            assert_equal route, r
            assert_equal({ action: "bar" }.merge(expected), params)
            called = true
          end

          assert called
        end
      end

      {
        segment: ["/a%2Fb%20c+d/splat", { segment: "a/b c+d", splat: "splat"   }],
        splat: ["/segment/a/b%20c+d", { segment: "segment", splat: "a/b c+d" }]
      }.each do |name, (request_path, expected)|
        define_method("test_recognize_#{name}") do
          get "/:segment/*splat", to: "foo#bar"

          env = rails_env "PATH_INFO" => request_path
          called = false
          route = @routes.first

          router.recognize(env) do |r, params|
            assert_equal route, r
            assert_equal(expected.merge(controller: "foo", action: "bar"), params)
            called = true
          end

          assert called
        end
      end

      def test_namespaced_controller
        get "/:controller(/:action(/:id))", controller: /.+?/
        route = @routes.first

        env = rails_env "PATH_INFO" => "/admin/users/show/10"
        called   = false
        expected = {
          controller: "admin/users",
          action: "show",
          id: "10"
        }

        router.recognize(env) do |r, params|
          assert_equal route, r
          assert_equal(expected, params)
          called = true
        end
        assert called
      end

      def test_recognize_literal
        get "/books(/:action(.:format))", controller: "books"
        route = @routes.first

        env = rails_env "PATH_INFO" => "/books/list.rss"
        expected = { controller: "books", action: "list", format: "rss" }
        called = false
        router.recognize(env) do |r, params|
          assert_equal route, r
          assert_equal(expected, params)
          called = true
        end

        assert called
      end

      def test_recognize_head_route
        match "/books(/:action(.:format))", via: "head", to: "foo#bar"

        env = rails_env(
          "PATH_INFO" => "/books/list.rss",
          "REQUEST_METHOD" => "HEAD"
        )

        called = false
        router.recognize(env) do |r, params|
          called = true
        end

        assert called
      end

      def test_recognize_head_request_as_get_route
        get "/books(/:action(.:format))", to: "foo#bar"

        env = rails_env "PATH_INFO" => "/books/list.rss",
                        "REQUEST_METHOD" => "HEAD"

        called = false
        router.recognize(env) do |r, params|
          called = true
        end

        assert called
      end

      def test_recognize_cares_about_get_verbs
        match "/books(/:action(.:format))", to: "foo#bar", via: :get

        env = rails_env "PATH_INFO" => "/books/list.rss",
                        "REQUEST_METHOD" => "POST"

        called = false
        router.recognize(env) do |r, params|
          called = true
        end

        assert_not called
      end

      def test_recognize_cares_about_post_verbs
        match "/books(/:action(.:format))", to: "foo#bar", via: :post

        env = rails_env "PATH_INFO" => "/books/list.rss",
                        "REQUEST_METHOD" => "POST"

        called = false
        router.recognize(env) do |r, params|
          called = true
        end

        assert called
      end

      def test_multi_verb_recognition
        match "/books(/:action(.:format))", to: "foo#bar", via: [:post, :get]

        %w( POST GET ).each do |verb|
          env = rails_env "PATH_INFO" => "/books/list.rss",
            "REQUEST_METHOD" => verb

          called = false
          router.recognize(env) do |r, params|
            called = true
          end

          assert called
        end

        env = rails_env "PATH_INFO" => "/books/list.rss",
          "REQUEST_METHOD" => "PUT"

        called = false
        router.recognize(env) do |r, params|
          called = true
        end

        assert_not called
      end

      def test_eager_load_with_routes
        get "/foo-bar", to: "foo#bar"
        assert_nil router.eager_load!
      end

      def test_eager_load_without_routes
        assert_nil router.eager_load!
      end

      private
        def _generate(route_name, options, recall)
          if recall
            options = options.merge(_recall: recall)
          end
          path = @route_set.path_for(options, route_name)
          uri = URI.parse path
          params = Rack::Utils.parse_nested_query(uri.query).symbolize_keys
          [uri.path, params]
        end

        def get(*args)
          ActiveSupport::Deprecation.silence do
            mapper.get(*args)
          end
        end

        def match(*args)
          ActiveSupport::Deprecation.silence do
            mapper.match(*args)
          end
        end

        def rails_env(env, klass = ActionDispatch::Request)
          klass.new(rack_env(env))
        end

        def rack_env(env)
          {
            "rack.version"      => [1, 1],
            "rack.input"        => StringIO.new,
            "rack.errors"       => StringIO.new,
            "rack.multithread"  => true,
            "rack.multiprocess" => true,
            "rack.run_once"     => false,
            "REQUEST_METHOD"    => "GET",
            "SERVER_NAME"       => "example.org",
            "SERVER_PORT"       => "80",
            "QUERY_STRING"      => "",
            "PATH_INFO"         => "/content",
            "rack.url_scheme"   => "http",
            "HTTPS"             => "off",
            "SCRIPT_NAME"       => "",
            "CONTENT_LENGTH"    => "0"
          }.merge env
        end
    end
  end
end
