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

cs__scoped_require 'contrast/utils/object_share'
cs__scoped_require 'contrast/api'

module Contrast
  module Utils
    # Utilities for converting ruby stack trace into DTMs
    class StackTraceUtils
      # need lib here to separate from specs
      AGENT_CLASS_MARKER = '/lib/contrast/'

      class << self
        # Determine if this method is being invoked by application code or not
        # based on the immediate caller of the code after Contrast.
        #
        # @return [Boolean] true if this code is called with application
        #   (non-gem) code in the caller
        def custom_code_context?
          root_dir = Contrast::Agent.framework_manager.app_root

          stack = Kernel.caller(0, 15)
          i = 0
          while i < stack.length
            stack_element = stack[i]
            i += 1
            next if stack_element.include?('/contrast/')

            return stack_element.start_with?(root_dir)
          end
        end

        # Call and translate a caller_locations array to an array of
        # StackTraceElement for TeamServer to display, excluding any Contrast
        # code found.
        #
        # @return [Array<Contrast::Api::Dtm::StackTraceElement]
        def build_protect_stack_array
          stack = caller(2, 20)
          return [] unless stack

          stack = reject_caller_entries(stack)
          i = 0
          stack.map! do |entry|
            element = Contrast::Api::Dtm::StackTraceElement.new
            element = fill_protect_element(element, entry, i)
            i += 1
            element
          end
          stack.compact!
          stack
        end

        # Translate a caller array to an array of TraceStacks for TeamServer to
        # display, excluding any Contrast code found.
        #
        # @param stack [Array<String>] the output of Kernel.caller
        # @return [Array<Contrast::Api::Dtm::TraceStack]
        def build_assess_stack_array stack
          converted = []
          return converted unless stack

          i = 0
          while i < stack.length
            caller_location = stack[i]
            i += 1
            next if caller_location.include?(AGENT_CLASS_MARKER)

            # To play nice with the way that TeamServer is rendering these
            # values, we only populate the file_name field with exactly what we
            # want them to display
            element = Contrast::Api::Dtm::TraceStack.new
            element.file_name = caller_location
            converted << element
          end
          converted
        end

        private

        def reject_caller_entries stack
          stack.reject do |entry|
            entry = entry.to_s
            entry.include?(AGENT_CLASS_MARKER)
          end
        end

        # In Contrast UI - there are many ways to render the stacktraces. For
        # Protect, there are two ways we're concerned about, one for the first
        # element and then all others.
        # 1 for protect first frame - We need the method specifically for this rendering
        # 1 for protect Nth frames
        def fill_protect_element element, caller_location, index
          return element unless caller_location

          if index.zero?
            element.method_name = caller_location.split('`')[-1].delete_suffix!('\'')
            element.file_name = caller_location.split(':')[0]
          else
            element.file_name = caller_location
          end

          element
        rescue StandardError
          # NOOP
          nil
        end
      end
    end
  end
end