//  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
//  ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
//  OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
//  THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
//  EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
//  OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
//  OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
//  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

/**
 * @description Apex Test Data Factory
 * @author benahm
 * @since 2017
 * @group Apex Test Data Factory
 */
@IsTest
public class TestDataFactory {
  /** Default value provider instance */
  private static DefaultValueProvider dvPrvdr = new DefaultValueProvider();

  /** Default value generator, used to auto-generate default values for non required fields  */
  public static final IFieldDefaultValue DEFAULT_VALUE = new AutoFieldDefaultValue();

  /** Constant that contains a dot '.' */
  private static final String DOT = '.';

  /** Map of sObject types with their corresponding map of field Tokens  */
  @TestVisible
  private static Map<Schema.SObjectType, Map<String, Schema.SObjectField>> mapOfFieldTokensMap = new Map<Schema.SObjectType, Map<String, Schema.SObjectField>>();

  /* ===========================  Properties =========================== */
  /** Default Value Provider property, used to override the default value provider instance  **/
  public static DefaultValueProvider defaultValueProvider {
    get {
      return dvPrvdr;
    }
    set {
      if (value != null) {
        dvPrvdr = value;
      }
    }
  }

  /** Determine if person account is enabled in current org **/
  @TestVisible
  private static Boolean isPersonAccountEnabled {
    get {
      return Schema.SObjectType.Account.fields.getMap()
        .containsKey('isPersonAccount');
    }
  }

  /* ===========================  Public methods =========================== */
  /**
   * @description create an sObject
   * @param sObjectName (String): name of the sObject to be created
   * @return sObject instance
   * @example
   * Account acc = (Account)TestDataFactory.createSObject('Account');
   */
  public static SObject createSObject(String sObjectName) {
    return createSObject(sObjectName, new Map<String, Object>(), true);
  }

  /**
   * @description create an sObject
   * @param sObjectName (String): name of the sObject to be created
   * @param Boolean (Boolean): if true the sObject instance is inserted to the Salesforce database
   * @return sObject instance
   * @example
   * Account acc = (Account)TestDataFactory.createSObject('Account',false);
   */
  public static SObject createSObject(String sObjectName, Boolean doInsert) {
    return createSObject(sObjectName, new Map<String, Object>(), doInsert);
  }

  /**
   * @description create an sObject with a map to override field values
   * @param sObjectName (String): name of the sObject to be created
   * @param mapOfValuesOverride (Map<String,Object>): map of values
   * @return sObject instance
   * @example
   * Account acc = (Account)TestDataFactory.createSObject('Account',new Map<String,Object>{
   *  'Description' => 'Test'
   * });
   */
  public static SObject createSObject(
    String sObjectName,
    Map<String, Object> mapOfValuesOverride
  ) {
    return createSObject(sObjectName, mapOfValuesOverride, true);
  }

  /**
   * @description create an sObject with a map to override field values
   * @param sObjectName (String): name of the sObject to be created
   * @param mapOfValuesOverride (Map<String,Object>): map of values
   * @param doInsert (Boolean): if true the sObject instance is inserted to the Salesforce database
   * @return sObject instance
   * @example
   * Account acc = (Account)TestDataFactory.createSObject('Account',new Map<String,Object>{
   *  'Description' => 'Test'
   * },false);
   */
  public static SObject createSObject(
    String sObjectName,
    Map<String, Object> mapOfValuesOverride,
    Boolean doInsert
  ) {
    return createSObjectList(sObjectName, mapOfValuesOverride, 1, doInsert)
      .get(0);
  }

  /**
   * @description create a list of sObjects
   * @param sObjectName (String): name of the sObject to be created
   * @param numberOfSObjects (Integer): number of sObjects to be created
   * @return list of sObject instances
   * @example
   * List<Account> accList = TestDataFactory.createSObjectList('Account',10);
   */
  public static List<SObject> createSObjectList(
    String sObjectName,
    Integer numberOfSObjects
  ) {
    return createSObjectList(
      sObjectName,
      new Map<String, Object>(),
      numberOfSObjects,
      true
    );
  }

  /**
   * @description create a list of sObjects
   * @param sObjectName (String): name of the sObject to be created
   * @param numberOfSObjects (Integer): number of sObjects to be created
   * @param doInsert (Boolean): if true the sObject list instances is inserted to the Salesforce database
   * @return list of sObject instances
   * @example
   * List<Account> accList = TestDataFactory.createSObjectList('Account',10,false);
   */
  public static List<SObject> createSObjectList(
    String sObjectName,
    Integer numberOfSObjects,
    Boolean doInsert
  ) {
    return createSObjectList(
      sObjectName,
      new Map<String, Object>(),
      numberOfSObjects,
      doInsert
    );
  }

  /**
   * @description create a list of sObjects with a map to override field values
   * @param sObjectName (String): name of the sObject to be created
   * @param mapOfValuesOverride (Map<String,Object>): map of values
   * @param numberOfSObjects (Integer): number of sObjects to be created
   * @return list of sObject instances
   * @example
   * List<Account> accList = TestDataFactory.createSObjectList('Account',new Map<String,Object>{
   *  'Description' => 'Test'
   * },10);
   */
  public static List<SObject> createSObjectList(
    String sObjectName,
    Map<String, Object> mapOfValuesOverride,
    Integer numberOfSObjects
  ) {
    return createSObjectList(
      sObjectName,
      mapOfValuesOverride,
      numberOfSObjects,
      true
    );
  }

  /**
   * @description create a list of sObjects with a map to override field values
   * @param sObjectName (String): name of the sObject to be created
   * @param mapOfValuesOverride (Map<String,Object>): map of values
   * @param numberOfSObjects (Integer): number of sObjects to be created
   * @param doInsert (Boolean): if true the sObject list instances is inserted to the Salesforce database
   * @return list of sObject instances
   * @example
   * List<Account> accList = TestDataFactory.createSObjectList('Account',new Map<String,Object>{
   *  'Description' => 'Test'
   * },10,false);
   */
  public static List<SObject> createSObjectList(
    String sObjectName,
    Map<String, Object> mapOfValuesOverride,
    Integer numberOfSObjects,
    Boolean doInsert
  ) {
    Schema.SObjectType sObjectType = Schema.getGlobalDescribe()
      .get(sObjectName);
    SObjectFactory sObjFactory = new SObjectFactory(sObjectType);
    List<SObject> listSObjectInstance = sObjFactory.getListSObjectInstance(
      mapOfValuesOverride,
      doInsert,
      numberOfSObjects
    );
    return listSObjectInstance;
  }

  /* ======================================================================================================== */
  /* =======================================  inner classes & interfaces ==================================== */
  /* ======================================================================================================== */
  /**
   * @description interface ISObjectManager
   */
  public interface ISObjectManager {
    List<SObject> getMainSObjectList();
    void setSObject(Integer index, SObject sObj);
    SObject getSObject(Integer index);
    void setRelationship(Integer index, String relationshipName, SObject sObj);
    SObject getRelationship(Integer index, String relationshipName);
    void insertAll();
  }

  /**
   * @description class SObjectManager : manage all the sObjects instances and the insertion to the salesforce DB
   */
  public class SObjectManager implements ISObjectManager {
    /** Main sObject name */
    private String sObjectName = null;
    /** List of main sObject instances */
    @TestVisible
    private List<SObject> mainSObjectList = new List<SObject>();
    /** Map of relationship names with their related list of sObject instances */
    @TestVisible
    private Map<String, List<SObject>> relatedSObjMapList = new Map<String, List<SObject>>();
    /** List of ordered relationship names by dot notation */
    @TestVisible
    private List<String> orderedRelationshipList = new List<String>();

    /* ===========================  Error Messages =========================== */
    /** Unable to insert "{0}" records: {1} */
    private final String ERR_INSERT_SOBJECT = 'Unable to insert "{0}" records: \n{1}';

    /**
     * @description constructor
     * @return sObjectName (String): main sObject name
     */
    public SObjectManager(String sObjectName) {
      this.sObjectName = sObjectName;
    }

    /**
     * @description get the list of all main sObject instances
     * @return list of sObject instances
     */
    public List<SObject> getMainSObjectList() {
      return mainSObjectList;
    }

    /**
     * @description set or add an sObject to the main sObject list for a given index
     * @param index (Integer): index for which the sObject instance will be added or set
     * @param sObj (SObject): sObject instance to add or set to the main sObject list
     */
    public void setSObject(Integer index, SObject sObj) {
      if (index == mainSObjectList.size()) {
        mainSObjectList.add(sObj);
      } else {
        mainSObjectList.set(index, sObj);
      }
    }

    /**
     * @description get an sObject from the main sObject list for a given index
     * @param index (Integer): index for which the sObject instance will be returned
     * @return the returnd sObject instance
     */
    public SObject getSObject(Integer index) {
      return mainSObjectList.get(index);
    }

    /**
     * @description set or add a related sObject into the related sObject map list for a given index and relationship name
     * @param index (Integer): index for which the sObject instance will be added or set
     * @param fullyQualifiedRelationshipName (String): fully qualified relationship name for which the sObject instance will be added or set
     * @param sObj (SObject): sObject instance to add or set to the related sObject map list
     */
    public void setRelationship(
      Integer index,
      String fullyQualifiedRelationshipName,
      SObject sObj
    ) {
      String fullyQualifiedRelationshipNameLowerCase = fullyQualifiedRelationshipName.toLowerCase();
      // get related sObject list for the fully qualified relationship name
      List<SObject> relatedSObjList = relatedSObjMapList.get(
        fullyQualifiedRelationshipNameLowerCase
      );
      if (relatedSObjList == null) {
        // instantiate the list if null
        relatedSObjList = new List<SObject>();
        // put the list into the related sObject map list
        relatedSObjMapList.put(
          fullyQualifiedRelationshipNameLowerCase,
          relatedSObjList
        );
        // add the fully qualified relationship name to the list of ordered relationship names
        addRelationshipName(fullyQualifiedRelationshipNameLowerCase);
      }

      if (index == relatedSObjList.size()) {
        relatedSObjList.add(sObj);
      } else {
        relatedSObjList.set(index, sObj);
      }
    }

    /**
     * @description get a related sObject from the related sObject map list for a given index and relationship name
     * @param index (Integer): index for which the sObject instance will be returned
     * @param fullyQualifiedRelationshipName (String): fully qualified relationship name for which the sObject instance will be returned
     * @return the returnd sObject instance
     */
    public SObject getRelationship(
      Integer index,
      String fullyQualifiedRelationshipName
    ) {
      try {
        List<SObject> relatedSObjList = relatedSObjMapList.get(
          fullyQualifiedRelationshipName.toLowerCase()
        );
        return relatedSObjList.get(index);
      } catch (Exception e) {
        return null;
      }
    }

    /**
     * @description insert all the sObjects stored in the sObject Manager into the Salesforce database
     */
    public void insertAll() {
      // loop through the ordered relationship list
      for (Integer i = 0; i < orderedRelationshipList.size(); i++) {
        String fullyQualifiedRelationshipName = orderedRelationshipList.get(i);
        List<SObject> sObjParentList = relatedSObjMapList.get(
          fullyQualifiedRelationshipName
        );

        // insert parent sObject list
        insertList(sObjParentList, DOT + fullyQualifiedRelationshipName);

        // last relationship name in the dot notation
        String leafRelationshipName = (DOT + fullyQualifiedRelationshipName)
          .substringAfterLast(DOT);

        if (!fullyQualifiedRelationshipName.contains(DOT)) {
          // single relationship
          // link child sObject list to the main sObject list
          setChildParentRelationship(
            mainSObjectList,
            sObjParentList,
            fullyQualifiedRelationshipName
          );
        } else {
          // multiple relationships

          // fully qualified relationship name without the leaf relationship
          String fullyQualifiedRelationshipNameWithoutLeaf = fullyQualifiedRelationshipName.substringBeforeLast(
            DOT
          );
          // get the list of sObject of the fully qualified relationship name without the leaf relationship name
          List<SObject> sObjChildList = relatedSObjMapList.get(
            fullyQualifiedRelationshipNameWithoutLeaf
          );
          // link child sObject list to the parent sObject list
          setChildParentRelationship(
            sObjChildList,
            sObjParentList,
            leafRelationshipName
          );
        }
      }

      // insert main sObject list
      insertList(mainSObjectList, '');
    }

    /* ============================================================================================= */
    /* =======================================  private methods ==================================== */
    /* ============================================================================================= */

    /**
     * @description link a list of child sObject instances to a list of parent sObject instances for a given relationship name
     * @param sObjChildList (List<SObject>): list of child sObject instances
     * @param sObjParentList (List<SObject>): list of parent sObject instances
     * @param relationshipName (String): relationship name that links the child sObject to the parent sObject
     */
    @TestVisible
    private void setChildParentRelationship(
      List<SObject> sObjChildList,
      List<SObject> sObjParentList,
      String relationshipName
    ) {
      Integer index = 0;
      for (SObject sObjChild : sObjChildList) {
        // get the field name that links the child sObject to the parent sObject
        String relationshipFieldName = getFieldNameFromRelationshipName(
          relationshipName
        );
        // set field related to the relationship name (on the child sObject), with the Id of the parent sObject
        sObjChild.put(
          relationshipFieldName,
          sObjParentList.get(index).get('Id')
        );
        index++;
      }
    }

    /**
     * @description add relationship name to the ordered relationship list, ordered by the number of dots
     * @param fullyQualifiedRelationshipName (String): fully qualified relationship name (example contact.account)
     */
    @testVisible
    private void addRelationshipName(String fullyQualifiedRelationshipName) {
      Integer i = 0;
      for (String rName : orderedRelationshipList) {
        if (fullyQualifiedRelationshipName == rName)
          return; // already exists
        if (
          rName.countMatches('.') <
          fullyQualifiedRelationshipName.countMatches('.')
        ) {
          // insert depending of the number of dots
          orderedRelationshipList.add(i, fullyQualifiedRelationshipName);
          return;
        }
        i++;
      }
      // insert at the end
      orderedRelationshipList.add(fullyQualifiedRelationshipName);
    }

    /**
     * @description execute a DML insert operation on a list of sObjects
     * @param sObjList (List<SObject>): list of sObjects to be inserted
     * @param errorMessageMergeValue (String): merge value provided only for error message generation (example : .contact.account)
     */
    @testVisible
    private void insertList(
      List<SObject> sObjList,
      String errorMessageMergeValue
    ) {
      Database.SaveResult[] saveResults = Database.insert(sObjList, false);
      Integer i = 0;
      List<String> listErrors = new List<String>();
      // Iterate through the save results
      for (Database.SaveResult saveResult : saveResults) {
        if (!saveResult.isSuccess()) {
          // Iterate through the errors
          for (Database.Error error : saveResult.getErrors()) {
            Set<String> fieldNames = new Set<String>{ 'Name', 'RecordTypeId' };
            fieldNames.addAll(error.getFields());
            String listFieldValues = '';
            for (String fieldName : fieldNames) {
              Object fieldValue;
              try {
                fieldValue = sObjList.get(i).get(fieldName);
              } catch (Exception e) {
                /*  ignore errors */
              }
              if (fieldValue != null) {
                listFieldValues += fieldName + '=' + fieldValue + ', ';
              }
            }
            listFieldValues = listFieldValues.removeEnd(', ');
            listErrors.add(
              error.getStatusCode() +
                ': ' +
                error.getMessage() +
                ' ' +
                '[' +
                listFieldValues +
                ']'
            );
          }
          i++;
        }
      }
      if (!listErrors.isEmpty()) {
        throw new TestDataFactoryException(
          formatErrorMessage(
            ERR_INSERT_SOBJECT,
            new List<String>{
              sObjectName + errorMessageMergeValue,
              String.join(listErrors, '\n')
            },
            ''
          )
        );
      }
    }
  }

  /**
   * @description interface ISObjectFactory
   */
  public interface ISObjectFactory {
    ISObjectManager getSObjectManager();
    SObject getSObjectInstance(
      Map<String, Object> mapOfValuesOverride,
      Boolean doInsert
    );
    List<SObject> getListSObjectInstance(
      Map<String, Object> mapOfValuesOverride,
      Boolean doInsert,
      Integer numberOfSObjects
    );
  }

  /**
   * @description class SObjectFactory : manage the creation of all the sObjects instances
   */
  public class SObjectFactory implements ISObjectFactory {
    /** Main sObject type to instantiated */
    private Schema.SObjectType sObjectType = null;
    /** Main sObject Name to instantiated */
    private String sObjectName;
    /** Instance of SObject Manager to store all the created sObject instances */
    private SObjectManager sObjMgr;

    /* ===========================  Error Messages =========================== */
    /** Unable to find the sObject */
    private final String ERR_NOT_FOUND_SOBJECT = 'Unable to find the sObject';
    /** The field "{0}" does not exist on "{1}" or the value type provided is incorrect */
    private final String ERR_FIELD_NOT_EXIST = 'The field "{0}" does not exist on "{1}" or the value type provided is incorrect';
    /** The relationship "{0}" does not exist on "{1}" */
    private final String ERR_RELATIONSHIP_NOT_EXIST = 'The relationship "{0}" does not exist on "{1}"';
    /** Unable to get the value for the field "{0}" */
    private final String ERR_UNABLE_TO_GET_FIELD_VALUE = 'Unable to get the value for the field "{0}"';

    /**
     * @description construtor
     * @param sObjectType (Schema.SObjectType): main sObject type to be instantiated
     */
    public SObjectFactory(Schema.SObjectType sObjectType) {
      if (sObjectType == null) {
        throw new TestDataFactoryException(
          formatErrorMessage(ERR_NOT_FOUND_SOBJECT, new List<String>{}, '')
        );
      }
      this.sObjectType = sObjectType;
      this.sObjectName = sObjectType.getDescribe(
          SObjectDescribeOptions.DEFERRED
        )
        .getName();
      this.sObjMgr = new SObjectManager(this.sObjectName);
    }

    /**
     * @description get the SObject Manager instance
     * @return sObject Manager instance
     */
    public SObjectManager getSObjectManager() {
      return sObjMgr;
    }

    /**
     * @description generate an sObject instance and apply the override map of values
     * @param mapOfValuesOverride (Map<String,Object>): map of values to override when instanciating the sObject
     * @param doInsert (Boolean): if true the sObject list instances is inserted to the Salesforce database
     * @return sObject instance
     */
    public SObject getSObjectInstance(
      Map<String, Object> mapOfValuesOverride,
      Boolean doInsert
    ) {
      return getListSObjectInstance(mapOfValuesOverride, doInsert, 1).get(0);
    }

    /**
     * @description generate a list of sObject instances and apply the override map of values
     * @param mapOfValuesOverride (Map<String,Object>): map of values to override when instanciating the sObject
     * @param doInsert (Boolean): if true the sObject list instances is inserted to the Salesforce database
     * @param numberOfSObjects (Integer): number of sObjects to be instantiated
     * @return list of sObject instances
     */
    public List<SObject> getListSObjectInstance(
      Map<String, Object> mapOfValuesOverride,
      Boolean doInsert,
      Integer numberOfSObjects
    ) {
      // get the map of values formed from the union of the map of values override given by the user and the auto-generated map of values for the required fields
      Map<String, IFieldDefaultValue> mapOfValues = defaultValueProvider.getDefaultMapOfValues(
        sObjectType,
        mapOfValuesOverride
      );

      for (Integer i = 0; i < numberOfSObjects; i++) {
        initSObjectInstance(mapOfValues, i); // init sObject instance
      }

      if (doInsert) {
        sObjMgr.insertAll(); // insert all if doInsert = true
      }

      return sObjMgr.getMainSObjectList();
    }

    /* ============================================================================================= */
    /* =======================================  private methods ==================================== */
    /* ============================================================================================= */

    /**
     * @description initiate the main sObject instance with all related sObject and apply the map of values
     * @param mapOfValues (Map<String,IFieldDefaultValue>): map of values with key as field name and value as the default value to be used when instantiating the sObject.
     *           This map contains the default values provided as input by the user and the auto-generated default values for the required fields
     * @param recordIndex (Integer): index number (starting from 0) of each record when creating a list of sObjects. For single sObject creation recordIndex is default to 0
     */
    @TestVisible
    private void initSObjectInstance(
      Map<String, IFieldDefaultValue> mapOfValues,
      Integer recordIndex
    ) {
      // add the main sObject instance to the sObject Manager
      sObjMgr.setSObject(recordIndex, sObjectType.newSObject());

      for (String fullyQualifiedFieldName : mapOfValues.keySet()) {
        // loop through the map of values

        // get the field name from the fully qualified field name
        String fieldName = (DOT + fullyQualifiedFieldName)
          .substringAfterLast(DOT);
        // get the field value from the map of values
        Object fieldValue = getFieldValueFromMapOfValues(
          mapOfValues,
          fullyQualifiedFieldName,
          recordIndex
        );

        if (!fullyQualifiedFieldName.contains(DOT)) {
          // field name
          putField(
            sObjMgr.getSObject(recordIndex),
            fieldName,
            fieldValue,
            sObjectName
          );
        } else {
          // relationship
          // first relationship name in fully qualified relationship name
          String firstRelationshipName = fullyQualifiedFieldName.substringBefore(
            DOT
          );
          // fully qualified relationship name without the field name
          String fullyQualifiedRelationshipName = fullyQualifiedFieldName.substringBeforeLast(
            DOT
          );
          // generate the related sObject instances for a given fullyQualifiedRelationshipName
          SObject relatedSObj = getRelationshipInstance(
            fullyQualifiedRelationshipName,
            recordIndex
          );
          // apply the field value for the related sObject
          putField(
            relatedSObj,
            fieldName,
            fieldValue,
            sObjectName + DOT + fullyQualifiedRelationshipName
          );
          // link the related sObject to the main sObject
          putSObject(
            sObjMgr.getSObject(recordIndex),
            firstRelationshipName,
            sObjMgr.getRelationship(recordIndex, firstRelationshipName),
            sObjectName
          );
        }
      }
    }

    /**
     * @description Recursive method that initiate all the related sObjects of a given fully qualified relationship name
     * @param fullyQualifiedRelationshipName (String): fully qualified relationship name (example : Case.Contact.Account)
     * @param recordIndex (Integer): index number (starting from 0) of each record when creating a list of sObjects. For single sObject creation recordIndex is default to 0
     * @return main sObject instance with all related sObjects attached to it
     */
    @TestVisible
    private SObject getRelationshipInstance(
      String fullyQualifiedRelationshipName,
      Integer recordIndex
    ) {
      // get the sObject instance from the sObject Manager
      SObject sObj = sObjMgr.getRelationship(
        recordIndex,
        fullyQualifiedRelationshipName
      );
      // return the sObject instance if already exists in the sObject Manager
      if (sObj != null) {
        return sObj;
      }

      // last relationship name in the dot notation
      String leafRelationshipName = (DOT + fullyQualifiedRelationshipName)
        .substringAfterLast(DOT);

      if (!fullyQualifiedRelationshipName.contains(DOT)) {
        // single relationship
        sObj = getRelatedSObjectInstance(
          sObjectType,
          leafRelationshipName,
          sObjectName
        );
      } else {
        // multiple relationships

        // fully qualified relationship name without the leaf relationship
        String fullyQualifiedRelationshipNameWithoutLeaf = fullyQualifiedRelationshipName.substringBeforeLast(
          DOT
        );
        // generate the all the sObject instances for the fully qualified relationship name without the leaf relationship name
        SObject relatedSObj = getRelationshipInstance(
          fullyQualifiedRelationshipNameWithoutLeaf,
          recordIndex
        ); // <= recursion
        // merge value for error message display
        String errorMessageMergeValue =
          sObjectName +
          DOT +
          fullyQualifiedRelationshipNameWithoutLeaf;
        // get sObject instance for the leaf relationship
        sObj = getRelatedSObjectInstance(
          relatedSObj.getSObjectType(),
          leafRelationshipName,
          errorMessageMergeValue
        );
        // link the related instance of the leaf relationship to the related sObject of the fully qualified relationship name without the leaf relationship name
        putSObject(
          relatedSObj,
          leafRelationshipName,
          sObj,
          errorMessageMergeValue
        );
      }

      // add the sObject instance to the sObject Manager
      sObjMgr.setRelationship(
        recordIndex,
        fullyQualifiedRelationshipName,
        sObj
      );
      return sObj;
    }

    /**
     * @description get related sObject instance of an sObject type for a given relationship name
     * @param sObjectType (Schema.SObjectType): sObject type to which the generate sObject is related
     * @param relationshipName (String): relationship name that links the sObject type and the sObject instance that will be generated
     * @param errorMessageMergeValue (String): merge value provided only for error message generation (example : Contact.Account)
     * @return the generated related sObject instance
     */
    @TestVisible
    private SObject getRelatedSObjectInstance(
      Schema.SObjectType sObjectType,
      String relationshipName,
      String errorMessageMergeValue
    ) {
      Map<String, Schema.SObjectField> fieldsMap = getFieldTokensMap(
        sObjectType
      );
      Schema.DescribeFieldResult fieldDesc;
      SObject sObj;
      try {
        // get the related field describe
        fieldDesc = fieldsMap.get(
            getFieldNameFromRelationshipName(relationshipName)
          )
          .getDescribe();
        // get related sObject instance
        sObj = fieldDesc.getReferenceTo().get(0).newSObject();
      } catch (Exception e) {
        throw new TestDataFactoryException(
          formatErrorMessage(
            ERR_RELATIONSHIP_NOT_EXIST,
            new List<String>{ relationshipName, errorMessageMergeValue },
            e.getMessage()
          )
        );
      }
      return sObj;
    }

    /**
     * @description get the field value from the map of values for a given record index and fully qualified field name
     * @param mapOfValues (Map<String,IFieldDefaultValue>): map of values with key as field name and value as the default value to be used when instantiating the sObject.
     * @param fullyQualifiedFieldName (String): fully qualified field name (example : Contact.Account.Name)
     * @param recordIndex (Integer): index number (starting from 0) of each record when creating a list of sObjects. For single sObject creation recordIndex is default to 0
     * @return field value
     */
    @TestVisible
    private Object getFieldValueFromMapOfValues(
      Map<String, IFieldDefaultValue> mapOfValues,
      String fullyQualifiedFieldName,
      Integer recordIndex
    ) {
      Object fieldValue;
      try {
        // get the field value
        fieldValue = mapOfValues.get(fullyQualifiedFieldName)
          .getValue(recordIndex);
      } catch (Exception e) {
        throw new TestDataFactoryException(
          formatErrorMessage(
            ERR_UNABLE_TO_GET_FIELD_VALUE,
            new List<String>{ fullyQualifiedFieldName },
            e.getMessage()
          )
        );
      }
      return fieldValue;
    }

    /**
     * @description attach an sObject into a related sObject
     * @param sObj (SObject): main sObject instance
     * @param relationshipName (String): sObject name of the related sObject
     * @param sObjectValue (SObject): related sObject instance to be attached to the main sObject instance
     * @param errorMessageMergeValue (String): merge value provided only for error message generation (example : Contact.Account)
     * @return main sObject instance
     */
    @TestVisible
    private SObject putSObject(
      SObject sObj,
      String relationshipName,
      SObject sObjectValue,
      String errorMessageMergeValue
    ) {
      try {
        sObj.putSObject(relationshipName, sObjectValue);
      } catch (Exception e) {
        throw new TestDataFactoryException(
          formatErrorMessage(
            ERR_RELATIONSHIP_NOT_EXIST,
            new List<String>{ relationshipName, errorMessageMergeValue },
            e.getMessage()
          )
        );
      }
      return sObj;
    }

    /**
     * @description set field value in given sObject instance
     * @param sObj (SObject): main sObject instance
     * @param fieldName (String): field name of the field to set
     * @param fieldValue (Object): value of the field to set
     * @param errorMessageMergeValue (String): merge value provided only for error message generation (example : Contact.Account)
     * @return main sObject instance
     */
    @TestVisible
    private SObject putField(
      SObject sObj,
      String fieldName,
      Object fieldValue,
      String errorMessageMergeValue
    ) {
      try {
        sObj.put(fieldName, fieldValue);
      } catch (Exception e) {
        throw new TestDataFactoryException(
          formatErrorMessage(
            ERR_FIELD_NOT_EXIST,
            new List<String>{ fieldName, errorMessageMergeValue },
            e.getMessage()
          )
        );
      }
      return sObj;
    }
  }

  /**
   * @description interface IDefaultValueProvider
   */
  public interface IDefaultValueProvider {
    Map<String, IFieldDefaultValue> getDefaultMapOfValues(
      Schema.SObjectType sObjectType,
      Map<String, Object> mapOfValuesOverride
    );
  }

  /**
   * @description virtual class DefaultValueProvider : manage the creation of the defaultMap and getting field default values
   */
  public virtual class DefaultValueProvider implements IDefaultValueProvider {
    /* ===========================  Error Messages =========================== */
    /** List values is not supported for field {0} */
    private final String ERR_LIST_VALUE_NOT_SUPPORTED = 'List values is not supported for field {0}';
    /** Bad record type Id provided for Account */
    private final String ERR_BAD_RECORDTYPE_ID_FOR_ACCOUNT = 'Bad record type Id provided for Account';

    /* ===========================  Properties =========================== */
    /** Person Account record type Ids */
    @TestVisible
    private Set<Id> personAccountRecordTypes {
      get {
        if (personAccountRecordTypes == null) {
          if (isPersonAccountEnabled) {
            String account = 'Account';
            personAccountRecordTypes = new Map<Id, SObject>(
                Database.query(
                  'select Id from RecordType where SObjectType=:account and isPersonType=true'
                )
              )
              .keySet();
          } else {
            personAccountRecordTypes = new Set<Id>();
          }
        }
        return personAccountRecordTypes;
      }
      set {
      }
    }

    /** Account default record type Id */
    @TestVisible
    private Id accountDefaultRecordTypeId {
      get {
        if (accountDefaultRecordTypeId == null) {
          Schema.DescribeSObjectResult dsr = Account.SObjectType.getDescribe(
            SObjectDescribeOptions.DEFERRED
          );
          for (Schema.RecordTypeInfo rti : dsr.getRecordTypeInfos()) {
            if (rti.isDefaultRecordTypeMapping()) {
              return rti.getRecordTypeId();
            }
          }
        }
        return accountDefaultRecordTypeId;
      }
      set {
      }
    }

    /**
     * @description get the default map of values for a given sObjectType.
     *                        The default map of value contains fully qualified field name as a key and the field value wrapped in FieldDefaultValue instance
     * @param sObjectType (Schema.SObjectType): sObject Type for which the map of values will be generated
     * @param mapOfValuesOverride (Map<String,Object>): map of values that will override the the auto-generated map of values
     * @return map of values
     */
    public Map<String, IFieldDefaultValue> getDefaultMapOfValues(
      Schema.SObjectType sObjectType,
      Map<String, Object> mapOfValuesOverride
    ) {
      Map<String, IFieldDefaultValue> mapOfValues = new Map<String, IFieldDefaultValue>(
        convertMapValueTypes('', mapOfValuesOverride)
      );
      generateDefaultMapOfValues(sObjectType, mapOfValues, '');
      return mapOfValues;
    }

    /* ============================================================================================= */
    /* =======================================  private methods ==================================== */
    /* ============================================================================================= */

    /**
     * @description generate the default map of values for a given sObjectType.
     *               The default value map of values will contain the map of values overrided and generated value for all required fields of the sObjectType
     * @param sObjectType (Schema.SObjectType): sObject Type for which the map of values will generated
     * @param mapOfValues (Map<String,IFieldDefaultValue>): map of values
     * @param prefix (String): prefix of the relationship (example : Contact.Account.)
     */
    @TestVisible
    private void generateDefaultMapOfValues(
      Schema.SObjectType sObjectType,
      Map<String, IFieldDefaultValue> mapOfValues,
      String prefix
    ) {
      Schema.DescribeSObjectResult sObjectDesc = sObjectType.getDescribe(
        SObjectDescribeOptions.DEFERRED
      );
      Map<String, Schema.SObjectField> fieldsMap = getFieldTokensMap(
        sObjectType
      ); // get the field Tokens map
      Set<String> keySetOverride = mapOfValues.keySet();
      Set<String> relationshipKeySet = getRelationshipKeySet(
        keySetOverride,
        prefix
      );
      // get the defined required fields set
      Set<String> definedRequiredFields = getDefinedSObjectRequiredFields(
        sObjectType
      );
      // get the defined optional fields set
      Set<String> definedOptionalFields = getDefinedSObjectOptionalFields(
        sObjectType
      );
      // Manage Person Account Org enabled
      if (sObjectType == Account.sObjectType && isPersonAccountEnabled) {
        managePersonAccountOrgEnabled(
          mapOfValues,
          prefix,
          definedRequiredFields
        );
      }
      // loop through fields map
      for (String fieldName : fieldsMap.keySet()) {
        Schema.DescribeFieldResult fieldDesc = fieldsMap.get(fieldName)
          .getDescribe(); // get field describe
        String fieldNameLowerCase = fieldName.toLowerCase();
        // construct the fully qualified name of the field
        String fullyQualifiedFieldName = prefix + fieldNameLowerCase;
        // get the field value from the mapOfValues
        Object overrideFieldValue = mapOfValues.get(fullyQualifiedFieldName);
        if (overrideFieldValue != null) {
          // override value provided for the field
          if (overrideFieldValue instanceof AutoFieldDefaultValue) {
            // auto generate a field value
            if (fieldDesc.getRelationshipName() == null) {
              // field
              // put a fieldDefaultValue instance of the field in order to auto-generate the field default value
              mapOfValues.put(
                fullyQualifiedFieldName,
                new FieldDefaultValue(this, sObjectDesc, fieldDesc)
              );
            } else {
              // relationship
              // put a field relationship in the map of values
              putRelationshipField(mapOfValues, fieldDesc, prefix); // <== recursion
              // remove the AutoFieldDefaultValue instance from the map
              mapOfValues.remove(fullyQualifiedFieldName);
            }
          }
        } else {
          // no override value provided for the field
          // field default value required and field not mentioned in definedOptionalFields or field mentioned in definedRequiredFields
          Boolean isRequired =
            (isDefaultValueRequired(fieldDesc) &&
            !definedOptionalFields.contains(fieldNameLowerCase)) ||
            definedRequiredFields.contains(fieldNameLowerCase);
          if (fieldDesc.getRelationshipName() == null) {
            // field
            if (isRequired) {
              // put a fieldDefaultValue instance of the field in order to auto-generate the field default value
              mapOfValues.put(
                fullyQualifiedFieldName,
                new FieldDefaultValue(this, sObjectDesc, fieldDesc)
              );
            }
          } else {
            // relationship
            String relationshipName = fieldDesc.getRelationshipName()
              .toLowerCase();
            if (isRequired || relationshipKeySet.contains(relationshipName)) {
              // relationship required or mentioned in the relationshipKeySet
              // put a field relationship in the map of values
              putRelationshipField(mapOfValues, fieldDesc, prefix); // <== recursion
            }
          }
        }
      }
    }

    /**
     * @description put a relationship field into a map of values
     * @param mapOfValues (Map<String,IFieldDefaultValue>): map of values in which relationship field will be put
     * @param fieldDesc (Schema.DescribeFieldResult): field describe information information of the relationship field
     * @param prefix (String): prefix of the relationship field (example : Contact.Account.)
     */
    @TestVisible
    private void putRelationshipField(
      Map<String, IFieldDefaultValue> mapOfValues,
      Schema.DescribeFieldResult fieldDesc,
      String prefix
    ) {
      String relationshipName = fieldDesc.getRelationshipName().toLowerCase();
      String fullyQualifiedRelationshipName = prefix + relationshipName;
      // at least add (fullyQualifiedRelationshipName.id,null) to force the instantiation of the related sObject
      // in case the sObject has no required fields
      mapOfValues.put(
        fullyQualifiedRelationshipName + DOT + 'id',
        new FieldDefaultValue(null)
      );
      // get the related sObject instance
      SObjectType refSObjectType = fieldDesc.getReferenceTo().get(0);
      // generate a map for the related sObject
      generateDefaultMapOfValues(
        refSObjectType,
        mapOfValues,
        fullyQualifiedRelationshipName + DOT
      );
    }

    /**
     * @description manage the map of values and the defined required fields for Account if person account enabled org
     * @param mapOfValues (Map<String,IFieldDefaultValue>): map of values from which the field value id will be extracted
     * @param prefix (String): prefix of the relationship field (example : Contact.Account.)
     * @param definedRequiredFields (Set<String>): Set of defined required fields
     */
    @TestVisible
    private void managePersonAccountOrgEnabled(
      Map<String, IFieldDefaultValue> mapOfValues,
      String prefix,
      Set<String> definedRequiredFields
    ) {
      String fullyQualifieldRecordTypeIdFieldName = prefix + 'recordtypeid';
      IFieldDefaultValue recordTypeIdDefaultValue = mapOfValues.get(
        fullyQualifieldRecordTypeIdFieldName
      );
      if (recordTypeIdDefaultValue == null) {
        recordTypeIdDefaultValue = new FieldDefaultValue(null);
      }
      if (recordTypeIdDefaultValue instanceof FieldDefaultValue) {
        // instance of FieldDefaultValue
        Object recordTypeIdValue = recordTypeIdDefaultValue.getValue(0);
        if (recordTypeIdValue == null) {
          recordTypeIdValue = accountDefaultRecordTypeId;
          mapOfValues.put(
            fullyQualifieldRecordTypeIdFieldName,
            new FieldDefaultValue(accountDefaultRecordTypeId)
          );
        }
        try {
          Id recordTypeId = Id.valueOf((String) recordTypeIdValue);
          if (personAccountRecordTypes.contains(recordTypeId)) {
            // person account
            definedRequiredFields.add('lastname');
          } else {
            // business account
            definedRequiredFields.add('name');
          }
        } catch (Exception e) {
          throw new TestDataFactoryException(
            formatErrorMessage(
              ERR_BAD_RECORDTYPE_ID_FOR_ACCOUNT,
              new List<String>{},
              e.getMessage()
            )
          );
        }
      } else if (recordTypeIdDefaultValue instanceof ListFieldDefaultValue) {
        // instance of ListFieldDefaultValue
        throw new TestDataFactoryException(
          formatErrorMessage(
            ERR_LIST_VALUE_NOT_SUPPORTED,
            new List<String>{ fullyQualifieldRecordTypeIdFieldName },
            ''
          )
        );
      }
    }

    /**
     * @description get defined set of required fields for a given sObject type and record type id
     * @param sObjectType (Schema.SObjectType): sObject type for which the defined required fields will be returned
     * @param recordTypeValue (Object): record type id provided for to the sObject Type
     * @return set of defined required fields
     */
    @TestVisible
    private Set<String> getDefinedSObjectRequiredFields(
      Schema.SObjectType sObjectType
    ) {
      Set<String> definedRequiredFields = new Set<String>();
      Set<String> definedRequiredFieldsByDeveloper = defineSObjectRequiredFields(
        sObjectType
      );
      if (definedRequiredFieldsByDeveloper != null) {
        definedRequiredFields.addAll(
          convertSetToLowerCase(definedRequiredFieldsByDeveloper)
        );
      }

      return definedRequiredFields;
    }

    /**
     * @description get defined set of optional fields for a given sObject type
     * @param sObjectType (Schema.SObjectType): sObject type for which the defined optional fields will be returned
     * @param recordTypeValue (Object): record type id provided for to the sObject Type
     * @return set of defined optional fields
     */
    @TestVisible
    private Set<String> getDefinedSObjectOptionalFields(
      Schema.SObjectType sObjectType
    ) {
      Set<String> definedOptionalFields = new Set<String>();
      Set<String> definedOptionalFieldsByDeveloper = defineSObjectOptionalFields(
        sObjectType
      );
      if (definedOptionalFieldsByDeveloper != null) {
        definedOptionalFields.addAll(
          convertSetToLowerCase(definedOptionalFieldsByDeveloper)
        );
      }

      return definedOptionalFields;
    }

    /**
     * @description get relationship names keyset that starts with a given prefix
     * @param keySetOverride (Set<String>): keySet of fully qualified field names
     * @param prefix (String): prefix (example : Contact.Account.)
     * @return set of relationship names
     * @example
     * keySetOverride = {Contact.Account.Name,Contact.FirstName}
     * Prefix : Contact.
     * Result : {Account}
     */
    @TestVisible
    private Set<String> getRelationshipKeySet(
      Set<String> keySetOverride,
      String prefix
    ) {
      Set<String> relationshipKeySet = new Set<String>();
      String prefixLowerCase = prefix.toLowerCase();
      // loop through the keySet
      for (String fullyQualifiedFieldName : keySetOverride) {
        fullyQualifiedFieldName = fullyQualifiedFieldName.toLowerCase();
        if (fullyQualifiedFieldName.startsWith(prefixLowerCase)) {
          // starts with the prefix
          String fullyQualifiedFieldNameAfterPrefix =
            fullyQualifiedFieldName.substringAfter(prefixLowerCase) + '';
          if (fullyQualifiedFieldNameAfterPrefix.contains(DOT)) {
            // if there is a relationship after the prefix
            // add the relationship name after the prefix
            relationshipKeySet.add(
              fullyQualifiedFieldName.substringBetween(prefixLowerCase, DOT)
            );
          }
        }
      }
      return relationshipKeySet;
    }

    /**
     * @description converting a map from Map(key:String,value:Object) to Map(key:String,value:IFieldDefaultValue)
     * @param prefix (String): prefix (example : Contact.Account.)
     * @param mapOfValues (Map<String,Object>): map of values to convert
     * @return converted map
     */
    @TestVisible
    private Map<String, IFieldDefaultValue> convertMapValueTypes(
      String prefix,
      Map<String, Object> mapOfValues
    ) {
      Map<String, IFieldDefaultValue> convertedMap = new Map<String, IFieldDefaultValue>();
      if (mapOfValues == null) {
        return convertedMap;
      }
      for (String name : mapOfValues.keySet()) {
        if (name != null) {
          Object value = mapOfValues.get(name);
          String fullyQualifiedName = prefix + name.trim().toLowerCase();
          if (value instanceof IFieldDefaultValue) {
            // cast the value if already of type IFieldDefaultValue
            convertedMap.put(fullyQualifiedName, (IFieldDefaultValue) value);
          } else if (value instanceof Map<String, Object>) {
            // convert the sub map of values and putting into the main map of values
            convertedMap.putAll(
              convertMapValueTypes(
                fullyQualifiedName + DOT,
                (Map<String, Object>) value
              )
            );
          } else if (value instanceof SObject) {
            String fullyQualifiedFieldName = getFieldNameFromRelationshipName(
              fullyQualifiedName
            );
            if (value instanceof List<SObject>) {
              // cast to list of sObject and wrap it in a ListRelationshipFieldDefaultValue instance
              convertedMap.put(
                fullyQualifiedFieldName,
                new ListRelationshipFieldDefaultValue((List<SObject>) value)
              );
            } else {
              // wrap the value in a RelationshipFieldDefaultValue instance
              convertedMap.put(
                fullyQualifiedFieldName,
                new RelationshipFieldDefaultValue((SObject) value)
              );
            }
          } else if (value instanceof List<Object>) {
            // cast to list objects and wrap it in a ListFieldDefaultValue instance
            convertedMap.put(
              fullyQualifiedName,
              new ListFieldDefaultValue((List<Object>) value)
            );
          } else {
            // wrap the value in a FieldDefaultValue instance
            convertedMap.put(fullyQualifiedName, new FieldDefaultValue(value));
          }
        }
      }
      return convertedMap;
    }

    /**
     * @description converting a given set of String to set of lower case string values
     * @param setOfValues (Set<String>): set of values to convert to lower case
     * @return converted set
     */
    @TestVisible
    private Set<String> convertSetToLowerCase(Set<String> setOfValues) {
      Set<String> convertedSet = new Set<String>();
      if (setOfValues == null) {
        return convertedSet;
      }
      for (String value : setOfValues) {
        if (value != null) {
          convertedSet.add(value.trim().toLowerCase());
        }
      }
      return convertedSet;
    }

    /**
     * @description test if a field requires a default value
     * @param fieldDesc (Schema.DescribeFieldResult): field describe information
     * @return true if a default value is required
     */
    @TestVisible
    private Boolean isDefaultValueRequired(
      Schema.DescribeFieldResult fieldDesc
    ) {
      return !fieldDesc.isDefaultedOnCreate() && // does not have a defaut value on creation
        !fieldDesc.isAutoNumber() && // is not an auto number
        !fieldDesc.isNillable() && // is required
        fieldDesc.isCreateable(); // value can be set on creation
    }

    /**
     * @description generate a default value for a given field of an sObject
     *                       depending on the field type, the corresponding get{FieldType}DefaultValue method will be invoked to retrieve the default value
     * @param sObjectDesc (Schema.DescribeSObjectResult): sObject describe information
     * @param fieldDesc (Schema.DescribeFieldResult): field describe information
     * @param recordIndex (Integer): index number (starting from 0) of each record when creating a list of sObjects. For single sObject creation recordIndex is default to 0
     * @return the generated field default value
     */
    @TestVisible
    private Object getDefaultValue(
      Schema.DescribeSObjectResult sObjectDesc,
      Schema.DescribeFieldResult fieldDesc,
      Integer recordIndex
    ) {
      if (fieldDesc != null) {
        // DisplayType Enum -> https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_enum_Schema_DisplayType.htm
        switch on fieldDesc.getType() {
          when Base64 {
            return getBase64DefaultValue(sObjectDesc, fieldDesc, recordIndex);
          }
          when Boolean {
            return getCheckboxDefaultValue(sObjectDesc, fieldDesc, recordIndex);
          }
          when Currency {
            return getCurrencyDefaultValue(sObjectDesc, fieldDesc, recordIndex);
          }
          when Date {
            return getDateDefaultValue(sObjectDesc, fieldDesc, recordIndex);
          }
          when Datetime {
            return getDateTimeDefaultValue(sObjectDesc, fieldDesc, recordIndex);
          }
          when Email {
            return getEmailDefaultValue(sObjectDesc, fieldDesc, recordIndex);
          }
          when Location {
            return getGeolocationDefaultValue(
              sObjectDesc,
              fieldDesc,
              recordIndex
            );
          }
          when Integer, Double {
            return getNumberDefaultValue(sObjectDesc, fieldDesc, recordIndex);
          }
          when Percent {
            return getPercentDefaultValue(sObjectDesc, fieldDesc, recordIndex);
          }
          when Phone {
            return getPhoneDefaultValue(sObjectDesc, fieldDesc, recordIndex);
          }
          when Picklist {
            return getPicklistDefaultValue(sObjectDesc, fieldDesc, recordIndex);
          }
          when MultiPicklist {
            return getMultiPicklistDefaultValue(
              sObjectDesc,
              fieldDesc,
              recordIndex
            );
          }
          when String {
            return getTextDefaultValue(sObjectDesc, fieldDesc, recordIndex);
          }
          when TextArea {
            return getTextAreaDefaultValue(sObjectDesc, fieldDesc, recordIndex);
          }
          when Time {
            return getTimeDefaultValue(sObjectDesc, fieldDesc, recordIndex);
          }
          when URL {
            return getURLDefaultValue(sObjectDesc, fieldDesc, recordIndex);
          }
        }
      }
      return null;
    }

    /**
     * @description get the Saleforce default value for given picklist
     * @param fieldDesc (Schema.DescribeFieldResult): field describe information
     * @return The picklist default value on Salesforce
     */
    @TestVisible
    private String getSFDefaultPicklistValue(
      Schema.DescribeFieldResult fieldDesc
    ) {
      if (fieldDesc != null) {
        List<Schema.PicklistEntry> pickVals = fieldDesc.getPicklistValues();
        for (Schema.PicklistEntry pickVal : pickVals) {
          if (pickVal.isDefaultValue()) {
            return pickVal.getValue(); // default value
          }
        }
        // if no default value is set, return the first value
        return pickVals.get(0).getValue();
      }
      return null;
    }

    /* ============================================================================================= */
    /* =======================================  virtual methods ==================================== */
    /* ============================================================================================= */
    /**
     * @description define a set of required fields for a given sObject
     * @param sObjectType (Schema.SObjectType): sObject Type for which the required fields will be defined
     * @return set of defined required fields
     */
    public virtual Set<String> defineSObjectRequiredFields(
      Schema.SObjectType sObjectType
    ) {
      return null;
    }
    /**
     * @description define a set of optional fields for a given sObject
     * @param sObjectType (Schema.SObjectType): sObject Type for which the optional fields will be defined
     * @return set of defined optional fields
     */
    public virtual Set<String> defineSObjectOptionalFields(
      Schema.SObjectType sObjectType
    ) {
      return null;
    }
    /**
     * @description get the default value for Base64 field type
     * @param sObjectDesc (Schema.DescribeSObjectResult): sObject describe information
     * @param fieldDesc (Schema.DescribeFieldResult): field describe information
     * @param recordIndex (Integer): index number (starting from 0) of each record when creating a list of sObjects. For single sObject creation recordIndex is default to 0
     * @return base64 default value
     */
    public virtual Blob getBase64DefaultValue(
      Schema.DescribeSObjectResult sObjectDesc,
      Schema.DescribeFieldResult fieldDesc,
      Integer recordIndex
    ) {
      return Blob.valueOf(recordIndex.format());
    }
    /**
     * @description get the default value for checkbox field type
     * @param sObjectDesc (Schema.DescribeSObjectResult): sObject describe information
     * @param fieldDesc (Schema.DescribeFieldResult): field describe information
     * @param recordIndex (Integer): index number (starting from 0) of each record when creating a list of sObjects. For single sObject creation recordIndex is default to 0
     * @return checkbox default value
     */
    public virtual Boolean getCheckboxDefaultValue(
      Schema.DescribeSObjectResult sObjectDesc,
      Schema.DescribeFieldResult fieldDesc,
      Integer recordIndex
    ) {
      return false;
    }
    /**
     * @description get the default value for Currency field type
     * @param sObjectDesc (Schema.DescribeSObjectResult): sObject describe information
     * @param fieldDesc (Schema.DescribeFieldResult): field describe information
     * @param recordIndex (Integer): index number (starting from 0) of each record when creating a list of sObjects. For single sObject creation recordIndex is default to 0
     * @return currency default value
     */
    public virtual Decimal getCurrencyDefaultValue(
      Schema.DescribeSObjectResult sObjectDesc,
      Schema.DescribeFieldResult fieldDesc,
      Integer recordIndex
    ) {
      return recordIndex;
    }
    /**
     * @description get the default value for Date field type
     * @param sObjectDesc (Schema.DescribeSObjectResult): sObject describe information
     * @param fieldDesc (Schema.DescribeFieldResult): field describe information
     * @param recordIndex (Integer): index number (starting from 0) of each record when creating a list of sObjects. For single sObject creation recordIndex is default to 0
     * @return date default value
     */
    public virtual Date getDateDefaultValue(
      Schema.DescribeSObjectResult sObjectDesc,
      Schema.DescribeFieldResult fieldDesc,
      Integer recordIndex
    ) {
      return Date.today();
    }
    /**
     * @description get the default value for Datetime field type
     * @param sObjectDesc (Schema.DescribeSObjectResult): sObject describe information
     * @param fieldDesc (Schema.DescribeFieldResult): field describe information
     * @param recordIndex (Integer): index number (starting from 0) of each record when creating a list of sObjects. For single sObject creation recordIndex is default to 0
     * @return datetime default value
     */
    public virtual Datetime getDateTimeDefaultValue(
      Schema.DescribeSObjectResult sObjectDesc,
      Schema.DescribeFieldResult fieldDesc,
      Integer recordIndex
    ) {
      return Datetime.now();
    }
    /**
     * @description get the default value for Email field type
     * @param sObjectDesc (Schema.DescribeSObjectResult): sObject describe information
     * @param fieldDesc (Schema.DescribeFieldResult): field describe information
     * @param recordIndex (Integer): index number (starting from 0) of each record when creating a list of sObjects. For single sObject creation recordIndex is default to 0
     * @return email default value
     */
    public virtual String getEmailDefaultValue(
      Schema.DescribeSObjectResult sObjectDesc,
      Schema.DescribeFieldResult fieldDesc,
      Integer recordIndex
    ) {
      return 'test' + recordIndex.format() + '@email.com';
    }
    /**
     * @description get the default value for Geolocation field type
     * @param sObjectDesc (Schema.DescribeSObjectResult): sObject describe information
     * @param fieldDesc (Schema.DescribeFieldResult): field describe information
     * @param recordIndex (Integer): index number (starting from 0) of each record when creating a list of sObjects. For single sObject creation recordIndex is default to 0
     * @return geolocation default value
     */
    public virtual Location getGeolocationDefaultValue(
      Schema.DescribeSObjectResult sObjectDesc,
      Schema.DescribeFieldResult fieldDesc,
      Integer recordIndex
    ) {
      return Location.newInstance(0, 0);
    }
    /**
     * @description get the default value for Number field type
     * @param sObjectDesc (Schema.DescribeSObjectResult): sObject describe information
     * @param fieldDesc (Schema.DescribeFieldResult): field describe information
     * @param recordIndex (Integer): index number (starting from 0) of each record when creating a list of sObjects. For single sObject creation recordIndex is default to 0
     * @return number default value
     */
    public virtual Decimal getNumberDefaultValue(
      Schema.DescribeSObjectResult sObjectDesc,
      Schema.DescribeFieldResult fieldDesc,
      Integer recordIndex
    ) {
      return recordIndex;
    }
    /**
     * @description get the default value for Percent field type
     * @param sObjectDesc (Schema.DescribeSObjectResult): sObject describe information
     * @param fieldDesc (Schema.DescribeFieldResult): field describe information
     * @param recordIndex (Integer): index number (starting from 0) of each record when creating a list of sObjects. For single sObject creation recordIndex is default to 0
     * @return percent default value
     */
    public virtual Decimal getPercentDefaultValue(
      Schema.DescribeSObjectResult sObjectDesc,
      Schema.DescribeFieldResult fieldDesc,
      Integer recordIndex
    ) {
      return recordIndex;
    }
    /**
     * @description get the default value for Phone field type
     * @param sObjectDesc (Schema.DescribeSObjectResult): sObject describe information
     * @param fieldDesc (Schema.DescribeFieldResult): field describe information
     * @param recordIndex (Integer): index number (starting from 0) of each record when creating a list of sObjects. For single sObject creation recordIndex is default to 0
     * @return phone default value
     */
    public virtual String getPhoneDefaultValue(
      Schema.DescribeSObjectResult sObjectDesc,
      Schema.DescribeFieldResult fieldDesc,
      Integer recordIndex
    ) {
      return '01 23 45 67 89';
    }
    /**
     * @description get the default value for Picklist field type
     * @param sObjectDesc (Schema.DescribeSObjectResult): sObject describe information
     * @param fieldDesc (Schema.DescribeFieldResult): field describe information
     * @param recordIndex (Integer): index number (starting from 0) of each record when creating a list of sObjects. For single sObject creation recordIndex is default to 0
     * @return picklist default value
     */
    public virtual String getPicklistDefaultValue(
      Schema.DescribeSObjectResult sObjectDesc,
      Schema.DescribeFieldResult fieldDesc,
      Integer recordIndex
    ) {
      return getSFDefaultPicklistValue(fieldDesc);
    }
    /**
     * @description get the default value for Multipicklist field type
     * @param sObjectDesc (Schema.DescribeSObjectResult): sObject describe information
     * @param fieldDesc (Schema.DescribeFieldResult): field describe information
     * @param recordIndex (Integer): index number (starting from 0) of each record when creating a list of sObjects. For single sObject creation recordIndex is default to 0
     * @return multipicklist default value
     */
    public virtual String getMultiPicklistDefaultValue(
      Schema.DescribeSObjectResult sObjectDesc,
      Schema.DescribeFieldResult fieldDesc,
      Integer recordIndex
    ) {
      return getSFDefaultPicklistValue(fieldDesc);
    }
    /**
     * @description get the default value for Text field type
     * @param sObjectDesc (Schema.DescribeSObjectResult): sObject describe information
     * @param fieldDesc (Schema.DescribeFieldResult): field describe information
     * @param recordIndex (Integer): index number (starting from 0) of each record when creating a list of sObjects. For single sObject creation recordIndex is default to 0
     * @return text default value
     */
    public virtual String getTextDefaultValue(
      Schema.DescribeSObjectResult sObjectDesc,
      Schema.DescribeFieldResult fieldDesc,
      Integer recordIndex
    ) {
      return 'test' + recordIndex.format();
    }
    /**
     * @description get the default value for TextArea field type
     * @param sObjectDesc (Schema.DescribeSObjectResult): sObject describe information
     * @param fieldDesc (Schema.DescribeFieldResult): field describe information
     * @param recordIndex (Integer): index number (starting from 0) of each record when creating a list of sObjects. For single sObject creation recordIndex is default to 0
     * @return text area default value
     */
    public virtual String getTextAreaDefaultValue(
      Schema.DescribeSObjectResult sObjectDesc,
      Schema.DescribeFieldResult fieldDesc,
      Integer recordIndex
    ) {
      return 'test' + recordIndex.format();
    }
    /**
     * @description get the default value for Time field type
     * @param sObjectDesc (Schema.DescribeSObjectResult): sObject describe information
     * @param fieldDesc (Schema.DescribeFieldResult): field describe information
     * @param recordIndex (Integer): index number (starting from 0) of each record when creating a list of sObjects. For single sObject creation recordIndex is default to 0
     * @return time default value
     */
    public virtual Time getTimeDefaultValue(
      Schema.DescribeSObjectResult sObjectDesc,
      Schema.DescribeFieldResult fieldDesc,
      Integer recordIndex
    ) {
      return Time.newInstance(12, 0, 0, 0);
    }
    /**
     * @description get the default value for URL field type
     * @param sObjectDesc (Schema.DescribeSObjectResult): sObject describe information
     * @param fieldDesc (Schema.DescribeFieldResult): field describe information
     * @param recordIndex (Integer): index number (starting from 0) of each record when creating a list of sObjects. For single sObject creation recordIndex is default to 0
     * @return URL default value
     */
    public virtual String getURLDefaultValue(
      Schema.DescribeSObjectResult sObjectDesc,
      Schema.DescribeFieldResult fieldDesc,
      Integer recordIndex
    ) {
      return 'http://test' + recordIndex + '.com';
    }
  }

  /**
   * @description common interface fo the field default value classes
   */
  public interface IFieldDefaultValue {
    /**
     * @description get the value wrapped for a given record index
     */
    Object getValue(Integer recordIndex);
  }

  /**
   * @description wrapper for default value that will be auto generated
   */
  public class AutoFieldDefaultValue implements IFieldDefaultValue {
    /**
     * @description get the value wrapped for a given record index
     * @param recordIndex (Integer): index number (starting from 0) of each record when creating a list of sObjects. For single sObject creation recordIndex is default to 0
     * @return field default value
     */
    public Object getValue(Integer recordIndex) {
      return null;
    }
  }

  /**
   * @description wrapper for a field default value
   */
  @TestVisible
  private class FieldDefaultValue implements IFieldDefaultValue {
    /** Instance of the container class */
    private DefaultValueProvider dvPrvdr = null;
    /** Field value */
    private Object value = null;
    /** SObject describe information */
    private Schema.DescribeSObjectResult sObjectDesc = null;
    /** Field describe information */
    private Schema.DescribeFieldResult fieldDesc = null;

    /**
     * @description construtor to wrap a griven field value
     * @param value (Object): the value that will be wrapped
     */
    public FieldDefaultValue(Object value) {
      this.value = value;
    }

    /**
     * @description construtor to wrap a field value that will be generated based on the given sObject & Field describe informations
     * @param dvPrvdr (DefaultValueProvider): default value provider instance of the containing class DefaultValueProvider
     * @param sObjectDesc (Schema.DescribeSObjectResult): sObject describe information
     * @param fieldDesc (Schema.DescribeFieldResult): field describe information
     */
    public FieldDefaultValue(
      DefaultValueProvider dvPrvdr,
      Schema.DescribeSObjectResult sObjectDesc,
      Schema.DescribeFieldResult fieldDesc
    ) {
      this.dvPrvdr = dvPrvdr;
      this.sObjectDesc = sObjectDesc;
      this.fieldDesc = fieldDesc;
    }

    /**
     * @description get the wrapped value for a given record index
     * @param recordIndex (Integer): index number (starting from 0) of each record when creating a list of sObjects. For single sObject creation recordIndex is default to 0
     * @return field default value
     */
    public Object getValue(Integer recordIndex) {
      if (dvPrvdr != null) {
        // if default value provider is set
        // generate the value from the sObject & field describe informations
        return dvPrvdr.getDefaultValue(sObjectDesc, fieldDesc, recordIndex);
      } else {
        if (
          value instanceof String // if value is of type string merge the index value
        )
          return mergeValue((String) value, 'index', recordIndex.format());
        else
          return value;
      }
    }
  }

  /**
   * @description wrapper for a relationship field default value
   */
  @TestVisible
  private class RelationshipFieldDefaultValue implements IFieldDefaultValue {
    /** Field value */
    private SObject sObjectValue = null;

    /**
     * @description construtor to wrap a griven field sObject value
     * @param sObjectValue (Object): the sObject value that will be wrapped
     */
    public RelationshipFieldDefaultValue(SObject sObjectValue) {
      this.sObjectValue = sObjectValue;
    }

    /**
     * @description get the wrapped sObject value for a given record index
     * @param recordIndex (Integer): index number (starting from 0) of each record when creating a list of sObjects. For single sObject creation recordIndex is default to 0
     * @return Id of the sObject field default value
     */
    public Object getValue(Integer recordIndex) {
      return sObjectValue.Id;
    }
  }

  /**
   * @description wrapper for a list of field default values
   */
  @TestVisible
  private class ListFieldDefaultValue implements IFieldDefaultValue {
    /** Field list values */
    private List<Object> listValues = null;

    /**
     * @description construtor to wrap a griven field list of values
     * @param listValues (List<Object>): the list of values that will be wrapped
     */
    public ListFieldDefaultValue(List<Object> listValues) {
      this.listValues = listValues;
    }

    /**
     * @description get the wrapped value for a given record index
     * @param recordIndex (Integer): index number (starting from 0) of each record when creating a list of sObjects. For single sObject creation recordIndex is default to 0
     * @return value at recordIndex position of list field default values
     */
    public Object getValue(Integer recordIndex) {
      return listValues.get(Math.mod(recordIndex, listValues.size()));
    }

    /**
     * @description return the size of the wrapped list of values
     */
    public Integer size() {
      return listValues.size();
    }
  }

  /**
   * @description wrapper for a list of relationship field default values
   */
  @TestVisible
  private class ListRelationshipFieldDefaultValue implements IFieldDefaultValue {
    /** Field list sObject values */
    private List<SObject> listSObjectValues = null;

    /**
     * @description construtor to wrap a griven field list of sObject values
     * @param listSObjectValues (List<SObject>): the list of values that will be wrapped
     */
    public ListRelationshipFieldDefaultValue(List<SObject> listSObjectValues) {
      this.listSObjectValues = listSObjectValues;
    }

    /**
     * @description get the wrapped list sObject values for a given record index
     * @param recordIndex (Integer): index number (starting from 0) of each record when creating a list of sObjects. For single sObject creation recordIndex is default to 0
     * @return id of the sObject at recordIndex position of list relationship field default values
     */
    public Object getValue(Integer recordIndex) {
      return listSObjectValues.get(recordIndex).Id;
    }

    /**
     * @description return the size of the wrapped list of the sObject values
     */
    public Integer size() {
      return listSObjectValues.size();
    }
  }

  /* ===========================  Utility methods  =========================== */

  /**
   * @description get the field tokens map of an sObject type with caching
   * @param sObjectType (Schema.SObjectType): sObject type for which the map of field tokens will be returned
   * @return field tokens map
   */
  @TestVisible
  private static Map<String, Schema.SObjectField> getFieldTokensMap(
    Schema.SObjectType sObjectType
  ) {
    if (mapOfFieldTokensMap.containsKey(sObjectType)) {
      return mapOfFieldTokensMap.get(sObjectType);
    } else {
      Map<String, Schema.SObjectField> fieldTokensMap = sObjectType.getDescribe(
          SObjectDescribeOptions.DEFERRED
        )
        .fields.getMap();
      mapOfFieldTokensMap.put(sObjectType, fieldTokensMap);
      return fieldTokensMap;
    }
  }

  /**
   * @description get field name from a relationship name
   * @param relationshipName (String): relationship name
   * @return field name
   */
  @TestVisible
  private static String getFieldNameFromRelationshipName(
    String relationshipName
  ) {
    String relationshipNameLowerCase = relationshipName.toLowerCase();
    if (
      relationshipNameLowerCase.contains('__r') // custom relationship
    )
      return relationshipNameLowerCase.replace('__r', '__c');
    return relationshipNameLowerCase + 'id'; // standard relationship
  }

  /**
   * @description merge given value in a string
   * @param text (String): text with a merge value
   * @param name (String): name of the merge value
   * @param value (String): value to be merged
   * @return text with merged value
   */
  @TestVisible
  private static String mergeValue(String text, String name, String value) {
    if (text == null)
      return null;
    String pattern = '(?i)\\{!' + name + '\\}';
    return text.replaceAll(pattern, value);
  }

  /**
   * @description format an error message
   * @param message (String): error message
   * @param mergeValues (List<String>): values to be merge in the error message
   * @param originalMessage (String): original error message
   * @return error message
   */
  @TestVisible
  private static String formatErrorMessage(
    String message,
    List<String> mergeValues,
    String originalMessage
  ) {
    String errorMessage = String.format(message, mergeValues);
    // if originalMessage is passed as param it will be include in the errorMessage
    if (!String.isBlank(originalMessage))
      errorMessage += '\n' + 'Original error message : ' + originalMessage;
    return errorMessage;
  }

  /**
   * @description this is a dummy test method needed in order to create a managed package for TestDataFactory
   */
  @IsTest
  private static void dummyTest() {
  }

  /**
   * @description Test Data Factory Exception
   */
  public class TestDataFactoryException extends Exception {
  }
}
