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

require 'contrast/extension/module'
require 'contrast/agent/patching/policy/policy_node'

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,
        # specifically for those methods which result in the trigger of an
        # attack (indicate points in the application where uncontrolled user
        # input attempted to or did do damage).
        class TriggerNode < PolicyNode
          JSON_NAME = 'name'
          JSON_APPLICATOR = 'applicator'
          JSON_APPLICATOR_METHOD = 'applicator_method'
          JSON_REQUIRED_PROPS = 'required_properties'
          JSON_OPTIONAL_PROPS = 'optional_properties'
          JSON_ON_EXCEPTION = 'on_exception'

          attr_reader :applicator, :applicator_method, :on_exception, :optional_properties, :required_properties,
                      :rule_id

          NODE = 'Trigger'
          def initialize trigger_hash = {}, rule_hash = {}
            super(trigger_hash)
            @rule_id = rule_hash[JSON_NAME]
            @on_exception = rule_hash[JSON_ON_EXCEPTION] # returns nil in most cases
            @required_properties = rule_hash[JSON_REQUIRED_PROPS]
            @optional_properties = rule_hash[JSON_OPTIONAL_PROPS]
            @applicator = class_from_string(rule_hash[JSON_APPLICATOR])
            # if a unique applicator method is defined for this method (rare case), preference getting that one.
            # otherwise, fall back to the normal applicator method for this rule
            @applicator_method = (trigger_hash[JSON_APPLICATOR_METHOD] || rule_hash[JSON_APPLICATOR_METHOD]).to_sym
          end

          def node_class
            NODE
          end

          def validate
            super
            unless applicator.public_methods(false).any?(applicator_method)
              raise(ArgumentError,
                    "#{ id } did not have a proper applicator method: "\
                    "#{ applicator } does not respond to #{ applicator_method }. Unable to create.")
            end
            validate_properties
            validate_rule
          end

          def validate_properties
            if (required_properties & optional_properties).any?
              raise(ArgumentError,
                    "#{ rule_id } had overlapping elements between required and optional properties. Unable to create.")
            end
            if (properties.keys - (required_properties | optional_properties)).any?
              raise(ArgumentError, "#{ id } had an unexpected property. Unable to create.")
            end

            return unless (required_properties - properties.keys).any?

            raise(ArgumentError, "#{ id } did not have a required property. Unable to create.")
          end

          def validate_rule
            raise(ArgumentError, 'Unknown rule did not have a proper name. Unable to create.') unless rule_id
            raise(ArgumentError, "#{ id } did not have a proper applicator. Unable to create.") unless applicator

            unless applicator_method
              raise(ArgumentError, "#{ id } did not have a proper applicator method. Unable to create.")
            end
            unless required_properties
              raise(ArgumentError, "#{ id } did not have a proper set of required properties. Unable to create.")
            end
            return if optional_properties

            raise(ArgumentError, "#{ id } did not have a proper set of optional properties. Unable to create.")
          end

          private

          def class_from_string str
            return unless str

            Object.cs__const_get(str)
          end
        end
      end
    end
  end
end