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

require 'contrast/agent/assess/tracker'
require 'contrast/components/logger'
require 'contrast/utils/duck_utils'

module Contrast
  module Utils
    module Assess
      # TrackingUtil has methods for determining if a object is being tracked
      class TrackingUtil
        extend Contrast::Components::Logger::InstanceMethods

        class << self
          # Public interface to our tracking check, isolating the internals
          # required for recursion.
          #
          # @param obj [Object] the thing to check if tracked
          # @return [Boolean] if the obj, or something in it if a collection, is
          #   tracked.
          def tracked? obj
            _tracked?(obj, 0)
          end

          # Public interface to our tracking check, isolating the internals
          # required for recursion.
          #
          # @param obj [Object] the thing to check if tracked
          # @return [Boolean] if the obj, or something in it if a collection, is
          #   tracked.
          def trackable? obj
            _trackable?(obj, 0)
          end

          private

          # Sometimes things are nested inside of each other, such as an Array
          # holding a Hash, holding that Array. In those cases, rather than
          # entering an infinite loop, we'll break out.
          # Right now, that level of nesting has been arbitrarily set to 10.
          #
          # @param obj [Object] the thing to check if tracked
          # @param idx [Integer] the number of levels nested we've gone
          # @return [Boolean] if the obj, or something in it if a collection, is
          #   tracked.
          def _tracked? obj, idx
            return false if obj.nil?
            return false if idx > 10

            idx += 1
            if Contrast::Utils::DuckUtils.iterable_hash?(obj)
              obj.each_pair do |k, v|
                return true if _tracked?(k, idx) || _tracked?(v, idx)
              end
              false
            elsif Contrast::Utils::DuckUtils.iterable_enumerable?(obj)
              obj.any? do |ele|
                _tracked?(ele, idx) unless obj == ele
              end
            else
              Contrast::Agent::Assess::Tracker.tracked?(obj)
            end
          rescue StandardError => e
            # This is used to ask if a ton of objects are tracked. They may not
            # all be iterable. Bad things could happen in some cases, like when
            # checking a closed statement for SQL injection trigger events
            logger.warn('Failed to determine tracking', e, module: obj.cs__class)
            false
          end

          # Sometimes things are nested inside of each other, such as an Array
          # holding a Hash, holding that Array. In those cases, rather than
          # entering an infinite loop, we'll break out.
          # Right now, that level of nesting has been arbitrarily set to 10.
          #
          # @param obj [Object] the thing to check if trackable
          # @param idx [Integer] the number of levels nested we've gone
          # @return [Boolean] if the obj, or something in it if a collection, is
          #   trackable.
          def _trackable? obj, idx
            return false if obj.nil?
            return false if idx > 10

            idx += 1
            if Contrast::Utils::DuckUtils.iterable_hash?(obj)
              obj.each_pair do |k, v|
                return true if _trackable?(k, idx)
                return true if _trackable?(v, idx)
              end
              false
            elsif Contrast::Utils::DuckUtils.iterable_enumerable?(obj)
              obj.any? do |ele|
                _trackable?(ele, idx) unless obj == ele
              end
            else
              Contrast::Agent::Assess::Tracker.trackable?(obj)
            end
          rescue StandardError => e
            # This is used to ask if a ton of objects are tracked. They may not
            # all be iterable. Bad things could happen in some cases, like when
            # checking a closed statement for SQL injection trigger events
            logger.warn('Failed to determine trackable', e, module: obj.cs__class)
            false
          end
        end
      end
    end
  end
end