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

require 'contrast/components/scope'

module Contrast
  module Agent
    module Patching
      module Policy
        # This class functions to translate our policy.json into an actionable
        # Ruby object, allowing for dynamic patching over hardcoded patching.
        #
        # @abstract
        class PolicyNode
          include Contrast::Components::Scope::InstanceMethods

          attr_accessor :class_name, :instance_method, :method_name, :method_visibility
          attr_reader :properties, :method_scope

          def node_class
            raise NoMethodError, 'specify the type of the feature for which this node patches'
          end

          def feature
            raise NoMethodError, 'specify the name of the feature for which this node patches'
          end

          def initialize policy_hash = {}
            @class_name = policy_hash[JSON_CLASS_NAME]
            @instance_method = policy_hash[JSON_INSTANCE_METHOD]
            @method_name = policy_hash[JSON_METHOD_NAME]
            @method_scope = policy_hash[JSON_METHOD_SCOPE]
            @method_visibility = policy_hash[JSON_METHOD_VISIBILITY]
            @properties = policy_hash[JSON_PROPERTIES]
            symbolize
          end

          def id
            @_id ||= "#{ feature }:#{ node_class }:#{ class_name }#{ instance_method? ? '#' : '.' }#{ method_name }"
          end

          # Don't let nodes be created that will be missing things we need
          # later on. Really, if they don't have these things, they couldn't have
          # done their jobs anyway.
          def validate
            unless class_name
              raise(ArgumentError, "#{ node_class } #{ id } did not have a proper class name. Unable to create.")
            end
            unless method_name
              raise(ArgumentError, "#{ node_class } #{ id } did not have a proper method name. Unable to create.")
            end
            unless method_name.is_a?(Symbol)
              raise(ArgumentError, "#{ node_class } #{ id } has a non symbol @method_name value. Unable to create.")
            end

            unless method_visibility.is_a?(Symbol)
              raise(ArgumentError,
                    "#{ node_class } #{ id } has a non symbol @method_visibility value. Unable to create.")
            end
            unless method_scope.nil? || Contrast::Agent::Scope.valid_scope?(method_scope)
              raise(ArgumentError, "#{ node_class } #{ id } requires an undefined scope. Unable to create.")
            end

            nil
          end

          # just turns this into a ruby-ism
          def instance_method?
            instance_method
          end

          private

          # Convert strings to symbols here, once, to avoid doing so on every
          # comparison at runtime
          def symbolize
            @method_name = @method_name.to_sym if @method_name
            @method_visibility = @method_visibility.to_sym if @method_visibility
            @method_scope = @method_scope.to_sym if @method_scope
          end

          # The keys used to read from policy.json to create the individual
          # policy nodes. These are common across node types
          JSON_CLASS_NAME = 'class_name'
          JSON_INSTANCE_METHOD = 'instance_method'
          JSON_METHOD_NAME = 'method_name'
          JSON_METHOD_SCOPE = 'scope'
          JSON_METHOD_VISIBILITY = 'method_visibility'
          JSON_PROPERTIES = 'properties'
        end
      end
    end
  end
end