# Copyright (c) 2020 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details.
# frozen_string_literal: true

require 'contrast/api/dtm.pb'
require 'contrast/framework/base_support'
require 'contrast/framework/rails/patch/support'
require 'contrast/utils/string_utils'

module Contrast
  module Framework
    module Rails
      # Used when Rails is present to define framework specific behavior
      class Support < BaseSupport
        extend Contrast::Framework::Rails::Patch::Support

        class << self
          RAILS_MODULE_NAME_VERSION = Gem::Version.new('6.0.0')

          def detection_class
            'Rails'
          end

          def version
            ::Rails.version
          end

          def application_name
            app_class = ::Rails.application.cs__class
            # Rails version 6.0.0 deprecated Rails::Application#parent_name, in Rails 6.1.0 that method will be removed entirely
            # and instead we need to use parent_module_name
            return app_class.parent_module_name if Gem::Version.new(::Rails.version) >= RAILS_MODULE_NAME_VERSION

            app_class.parent_name
          end

          def application_root
            ::Rails.root
          end

          def server_type
            'rails'
          end

          def collect_routes
            find_all_routes(::Rails.application, [])
          end

          # Find the current route, based on the provided Request wrapper
          # @param request[Contrast::Agent::Request]
          # @return [Contrast::Api::Dtm::RouteCoverage]
          def current_route request
            return unless ::Rails.cs__respond_to?(:application)

            # returns array of arrays [[match_data, path_parameters, route]], sorted by
            # precedence
            # match_data: ActionDispatch::Journey::Path::Pattern::MatchData
            # path_parameters: hash of various things
            # route: ActionDispatch::Journey::Route
            full_routes = ::Rails.application.routes.router.send(:find_routes, request.rack_request)
            return if full_routes.empty?

            full_route = full_routes[0]

            # the route is directly implemented within the application
            if direct_route?(full_route)
              route = full_route[2] # route w/ highest precedence
              Contrast::Api::Dtm::RouteCoverage.from_action_dispatch_journey(route)
            else
              engine_route(full_route, request)
            end
          rescue StandardError => _e
            nil
          end

          def retrieve_request env
            rails_env = ::Rails.application.env_config.merge(env)
            ::ActionDispatch::Request.new(rails_env || env)
          end

          AC_INSTANCE = 'action_controller.instance'
          def streaming? env
            return false unless defined?(::ActionController::Live)

            env[AC_INSTANCE].cs__class.included_modules.include?(::ActionController::Live)
          end

          private

          # route is not mounted within an engine
          def direct_route? full_route
            full_route[2]&.app&.cs__class == ActionDispatch::Routing::RouteSet::Dispatcher ||
                (full_route[2].cs__class == ActionDispatch::Journey::Route && full_route[2]&.app&.cs__class == ActionDispatch::Routing::Mapper::Constraints)
          end

          def engine_route full_route, request
            engine_route = full_route[2] # supposed route - but actually an Engine mount point
            return unless engine_route

            engine_mount_name = engine_route.name
            return unless engine_mount_name

            engine_path_segments = request.rack_request.path_info.split(engine_mount_name)
            return if engine_path_segments.empty?

            path_within_engine = engine_path_segments[-1]
            return unless path_within_engine

            engine_router = engine_route.app&.app&.routes&.router
            return unless engine_router

            # Get all routes regardless of http method
            matching_routes = engine_router.send(:filter_routes, path_within_engine)
            return unless matching_routes

            # filter for current http method
            reportable_routes = engine_router.send(:match_routes, matching_routes, request.rack_request)
            return if reportable_routes.empty?

            Contrast::Api::Dtm::RouteCoverage.from_action_dispatch_journey(reportable_routes[0])
          end

          # Rails engine routes need to be detected by inspecting Engine class route set
          def find_all_routes app, route_list
            return route_list unless app.cs__respond_to?(:routes) && app.routes.cs__respond_to?(:routes)

            app.routes.routes.each do |route|
              if route.cs__respond_to?(:app) && route.app.cs__class == ActionDispatch::Routing::RouteSet::Dispatcher
                route_list << Contrast::Api::Dtm::RouteCoverage.from_action_dispatch_journey(route)
              elsif route.app.app.cs__respond_to?(:routes)
                route_list += find_all_routes(route.app.app, [])
              end
            end
            route_list
          end
        end
      end
    end
  end
end