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

module Contrast
  module Utils
    # used in Contrast::Agent::Response
    module ResponseUtils
      private

      HTTP_PREFIX = /^[Hh][Tt][Tt][Pp][_-]/i.cs__freeze

      # Given some holder of the content of the response's body, extract that
      # content and return it as a String
      #
      # @param body [String, Rack::File, Rack::BodyProxy,
      #   ActionDispatch::Response::RackBody, Rack::Response] Something that
      #   holds, wraps, or is the body of the Response
      # @return [nil, String] the content of the body
      def extract_body body
        return unless body
        return if body_is_file?(body)
        return if body_is_sprockets?(body)

        return handle_rack_body_proxy(body) if body.is_a?(Rack::BodyProxy)
        return extract_body(body.body) if sub_extractable?(body)
        return enumerable_text_from(body) if Contrast::Utils::DuckUtils.quacks_to?(body, :each)
        # https://stackoverflow.com/questions/15654676/how-to-convert-activesupportsafebuffer-to-string
        return body.to_str if body.is_a?(ActionView::OutputBuffer)

        read_or_string(body)
      end

      def sub_extractable? body
        (defined?(ActionDispatch::Response::RackBody) && body.is_a?(ActionDispatch::Response::RackBody)) ||
            body.is_a?(Rack::Response)
      end

      def enumerable_text_from body
        entries = body.map { |entry| read_or_string(entry) }
        entries.compact!
        entries.join(Contrast::Utils::ObjectShare::NEW_LINE)
      end

      def handle_rack_body_proxy body
        next_body = body.instance_variable_get(:@body)
        case next_body
        when Array
          extract_body(next_body[0])
        else
          extract_body(next_body)
        end
      end

      def read_or_string obj
        return unless obj

        if Contrast::Utils::DuckUtils.quacks_to?(obj, :read)
          tmp = obj.read
          obj.rewind
          tmp
        else
          obj.to_s
        end
      end

      # After Rack 3.1 gets live - line 88 (Rack::File) will be removed.
      # In 3.1 version, they drop the support for File class and will only support Files class
      #
      # @param body [String, Rack::File, Rack::BodyProxy,
      #   ActionDispatch::Response::RackBody, Rack::Response] Something that
      #   holds, wraps, or is the body of the Response
      def body_is_file? body
        return true if defined?(Rack::File) && (body.is_a?(Rack::File) || body.is_a?(Rack::File::Iterator))
        return true if defined?(Rack::Files) && (body.is_a?(Rack::Files) || body.is_a?(Rack::Files::Iterator))

        false
      end

      # Detects Rails' Sprockets asset pipeline objects passed as body.
      # Returns false if Sprockets is passed as body, the Agent does not
      # support Sprockets::Asset for body extraction.
      #
      # @param body [String, Rack::File, Rack::BodyProxy]
      def body_is_sprockets? body
        return body.cs__is_a?(Sprockets::Asset) if defined?(Sprockets) && defined?(Sprockets::Asset)

        false
      end
    end
  end
end