### Copyright 2017 Pixar

###
###    Licensed under the Apache License, Version 2.0 (the "Apache License")
###    with the following modification; you may not use this file except in
###    compliance with the Apache License and the following modification to it:
###    Section 6. Trademarks. is deleted and replaced with:
###
###    6. Trademarks. This License does not grant permission to use the trade
###       names, trademarks, service marks, or product names of the Licensor
###       and its affiliates, except as required to comply with Section 4(c) of
###       the License and to reproduce the content of the NOTICE file.
###
###    You may obtain a copy of the Apache License at
###
###        http://www.apache.org/licenses/LICENSE-2.0
###
###    Unless required by applicable law or agreed to in writing, software
###    distributed under the Apache License with the above modification is
###    distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
###    KIND, either express or implied. See the Apache License for the specific
###    language governing permissions and limitations under the Apache License.
###
###

module Chook

  # A subject that will be used with a TestEvent class, to be sent to
  # a Webhook server, simulating one from the JSS.
  #
  # This is the parent class to all classes in the Chook::TestSubjects module.
  #
  # All constants, methods, and attributes that are common to TestSubject
  # classes are defined here.
  #
  class TestSubject < Chook::Subject

    # Dynamically generate the varios subclasses to TestSubject
    # and store them in the TestSubjects module.
    def self.generate_classes
      Chook::Subject.classes.each do |classname, attribs|
        # Don't redefine anything.
        next if Chook::TestSubjects.const_defined? classname

        # new subclass of Chook::TestSubject
        new_class = Class.new(Chook::TestSubject) do
          # add getter, setters, validators for all the attribs.
          attribs.each do |attrib, deets|
            # getter
            attr_reader attrib
            validator = deets[:validation]

            # setter with validator
            define_method("#{attrib}=") do |new_val|
              if validator.is_a? Class
                raise "Invalid value for #{attrib}, must be a #{validator}" unless new_val.is_a? validator
              elsif !validator.nil?
                raise "Invalid value for #{attrib}" unless Chook::Validators.send validator, new_val
              end
              instance_variable_set(('@' + attrib.to_s), new_val)
            end # end define method
          end # end do |attrib, deets|
        end # end new_class
        # set a class constant so each class knows it's name
        new_class.const_set Chook::Subject::NAME_CONSTANT, classname

        # add the class to the Chook::TestSubjects module
        Chook::TestSubjects.const_set classname, new_class
      end # end Chook::Subject.classes.each do |classname, attribs|
    end # end generate_classes

    # a random test subject,
    def self.random
      random_vals = {}
      Chook::Subject.classes[self.const_get NAME_CONSTANT].each do |attrib, deets|
        random_vals[attrib] = Chook::Randomizers.send deets[:randomizer] if deets[:randomizer]
      end # each do |attr_def|
      new random_vals
    end # end random

    # a sampled test subject (real data from real JSS objects)
    # NOTE: a valid ruby-jss JSS::APIConnection must exist
    def self.sample(ids = 'random', api: JSS.api)
      classname = self.const_get Chook::Subject::NAME_CONSTANT
      ids = [ids] if ids.is_a? Integer
      # !!Kernel.const_get('JSS::' + classname) rescue false
      if classname == 'PatchSoftwareTitleUpdated'
        all_ids = Chook::Samplers.all_patch_ids('blah', api: api)
      else
        all_ids = Kernel.const_get('JSS::' + classname).all_ids(api: api) # (api: api)
      end
      ids = [all_ids.sample] if ids == 'random'

      ok = true
      if ids.is_a? Array
        ids.each { |id| ok == false unless id.is_a? Integer }
      else
        ok = false
      end
      raise 'ids must be an Array of Integers' unless ok

      raw_samples = []
      samples = []

      valid_ids = ids & all_ids
      raise "Invalid JSS IDs: #{ids}" if valid_ids.empty?

      valid_ids.each do |id|
        if classname == 'PatchSoftwareTitleUpdated'
          raw_samples << Chook::Samplers.all_patches(api: api).select { |patch| patch[:id] == id }
          raw_samples.flatten!
        else
          raw_samples << Kernel.const_get('JSS::' + classname).fetch(id: id, api: api)
        end
      end

      raw_samples.each do |sample|
        subject_details = {}
        Chook::Subject.classes[classname].map do |subject_key, details|
          extractor = details[:api_object_attribute]
          subject_details[subject_key] =
            case extractor
            when Symbol
              if classname == 'PatchSoftwareTitleUpdated'
                # If there is a sampler method available, call it.
                if details[:sampler]
                  extractor = details[:sampler]
                  Chook::Samplers.send(extractor, sample)
                else
                  # Otherwise use it like a hash key
                  sample[extractor]
                end
              else
                sample.send extractor
              end
            when Array
              extractor = extractor.dup # If this doesn't get duplicated, shift will change details[:api_object_attribute]
              method = extractor.shift
              raw_hash_keys = extractor
              method_result = sample.send(method)
              raw_hash_keys.each { |key| method_result = method_result[key] }
              method_result
            when Proc
              extractor.call sample
            end
        end # do |subject_key, details|
        samples << Kernel.const_get('Chook::TestSubjects::' + classname).new(subject_details)
      end # end samples.each do |sample|
      samples
    end # end sample

    def json_hash
      # Verify that input is a child of TestSubjects, raise if not
      raise 'Invalid TestSubject' unless self.class.superclass == Chook::TestSubject
      test_subject_attributes = Chook::Subject.classes[self.class.to_s.split('::')[-1]]
      raw_hash_form = {}
      test_subject_attributes.each do |attribute, details|
        if details.keys.include? :to_json
          raw_hash_form[attribute] = send(attribute).send details[:to_json]
        else
          raw_hash_form[attribute] = instance_variable_get('@' + attribute.to_s)
        end
      end # end test_subject_attributes.keys.each do |attribute, details|
      raw_hash_form
    end # end json_hash

    # All the subclassses will inherit this constructor
    #
    # The argument is a Hash with keys from the appropriate keys defiend
    # in Chook::Subject.classes
    #
    def initialize(subject_data = nil)
      classname = self.class.const_get Chook::Subject::NAME_CONSTANT
      attribs = Chook::Subject.classes[classname]

      if subject_data
        subject_data.each do |key, value|
          # ignore unknown attributes. Shouldn't get any, but...
          next unless attribs[key]

          # does the value need conversion?
          converter = attribs[key][:converter]
          if converter
            value = converter.is_a?(Symbol) ? value.send(converter) : converter.call(value)
          end # if converter
          instance_variable_set(('@' + key.to_s), value)
        end # each key value
      else
        attribs.keys.each { |key| instance_variable_set(('@' + key.to_s), nil) }
      end # if subject_data
    end # init

  end # class TestSubject

end # module