/*
  Forceea data factory (v2.9.1)
  Copyright (C) 2024 Nikos Mitrakis

  This program is free software: you can redistribute it and/or modify it under the terms
  of the GNU General Public License as published by the Free Software Foundation,
  either version 3 of the License, or any later version.

  This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
  without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
  See the GNU General Public License for more details.

  You can find a copy of the GNU General Public License at
  https://github.com/Forceea/Forceea-data-factory/blob/master/LICENSE
*/
@IsTest
@SuppressWarnings('PMD.MethodNamingConventions')
public class ForceeaGeneratorSerialTest {

    @IsTest
    private static void serialDate_positiveStep_expectSuccess() {
        Integer numRecords = 100;
        Date fromValue = Date.newInstance(2018, 1, 1);
        Integer stepValue = 5;

        Test.startTest();
        FObject obj = new FObject(FObjectOne__c.SObjectType, numRecords);
        obj.setDefinition(FObjectOne__c.TestDate__c, 'Serial Type(Date) From(2018-01-01) Step(5)');
        List<FObjectOne__c> records = (List<FObjectOne__c>) obj.getRecords();
        Test.stopTest();

        Date expectedResult = fromValue.addDays((numRecords - 1) * stepValue);
        Date actualResult = records[numRecords - 1].TestDate__c;
        System.assertEquals(expectedResult, actualResult, 'serial date - errors found');
    }

    @IsTest
    private static void serialDate_negativeStep_expectSuccess() {
        Integer numRecords = 100;
        Date fromValue = Date.newInstance(2018, 1, 1);
        Integer stepValue = -5;

        Test.startTest();
        FObject obj = new FObject(FObjectOne__c.SObjectType, numRecords)
            .setDefinition(FObjectOne__c.TestDate__c, 'Serial Type(Date) From(2018-01-01) Step(-5)');
        List<FObjectOne__c> records = (List<FObjectOne__c>) obj.getRecords();
        Test.stopTest();

        Date expectedResult = fromValue.addDays((numRecords - 1) * stepValue);
        Date actualResult = records[numRecords - 1].TestDate__c;
        System.assertEquals(expectedResult, actualResult, 'serial date - errors found');
    }

    @IsTest
    private static void serialDate_fromDatetime_expectSuccess() {
        Integer numRecords = 100;
        Date fromValue = Date.newInstance(2018, 1, 1);
        Integer stepValue = 2;

        Test.startTest();
        FObject obj = new FObject(FObjectOne__c.SObjectType, numRecords)
            .setDefinition(FObjectOne__c.TestDate__c, 'Serial Type(Date) From(2018-01-01 02:02:02) Step(2)');
        List<FObjectOne__c> records = (List<FObjectOne__c>) obj.getRecords();
        Test.stopTest();

        Date expectedResult = fromValue.addDays((numRecords - 1) * stepValue);
        Date actualResult = records[numRecords - 1].TestDate__c;
        System.assertEquals(expectedResult, actualResult, 'serial date - errors found');
    }

    @IsTest
    private static void serialDate_decimalStep_expectFailure() {
        FObject.setExceptionHandling('continue');

        Test.startTest();
        FObject obj = new FObject(FObjectOne__c.SObjectType, 10)
            .setDefinition(FObjectOne__c.TestDate__c, 'Serial Type(Date) From(2018-01-01) Step(2.2)');
        obj.insertRecords(true);
        Test.stopTest();

        String expectedResult = 'Invalid definition [Serial Type(Date) ' +
            'From(2018-01-01) Step(2.2)] of field [TestDate__c]';
        String actualResult = obj.getErrors().get('TestDate__c').get(0);
        System.assertEquals(expectedResult, actualResult, 'serial date - errors found');
    }

    @IsTest
    private static void serialDate_serializationInSameFObject_expectSuccess() {
        Integer numRecords = 10;

        Test.startTest();
        // create some serialized records
        FObject obj = new FObject(FObjectTwo__c.SObjectType, numRecords)
            .setDefinition(FObjectTwo__c.TestPicklistDRequired__c, 'random type(picklist)')
            .setDefinition(FObjectTwo__c.TestTextUnique__c, 'serial type(date) from(2020-1-1) step(1)');
        List<FObjectOne__c> records = obj.getRecords();

        // create some more serialized records of the same FObject
        obj.setDefinition(FObjectTwo__c.TestText__c, 'static value(test)');
        records.addAll((List<FObjectOne__c>) obj.getRecords());

        // insert records
        String errorMessage = '';
        try {
            obj.insertRecords(true);
        } catch (Exception ex) {
            errorMessage = ex.getMessage();
        }
        Test.stopTest();

        Boolean result = FObject.processIsValid() && String.isBlank(errorMessage);
        System.assert(result, 'serial data - errors found');
    }

    @IsTest
    private static void serialDate_serializationInDifferentFObjects_expectSuccess() {
        Integer numRecords = 10;

        Test.startTest();
        // create some serialized records
        FObject obj1 = new FObject(FObjectTwo__c.SObjectType, numRecords)
            .setDefinition(FObjectTwo__c.TestPicklistDRequired__c, 'random type(picklist)')
            .setDefinition(FObjectTwo__c.TestTextUnique__c, 'serial type(date) from(2020-1-1) step(1)');
        obj1.getRecords();

        // create some more serialized records of a different FObject
        FObject obj2 = new FObject(FObjectTwo__c.SObjectType, numRecords)
            .setDefinition(FObjectTwo__c.TestPicklistDRequired__c, 'random type(picklist)')
            .setDefinition(FObjectTwo__c.TestTextUnique__c, 'serial type(date) from(2020-1-1) step(1)');
        obj2.getRecords();

        // insert records
        String errorMessage = '';
        try {
            obj1.insertRecords(true);
            obj2.insertRecords(true);
        } catch (Exception ex) {
            errorMessage = ex.getMessage();
        }
        Test.stopTest();

        Boolean result = FObject.processIsValid() && String.isBlank(errorMessage);
        System.assert(result, 'serial number - errors found');
    }

    @IsTest
    private static void serialDateVirtualField_valid_expectSuccess() {
        Integer numRecords = 10;
        Date fromValue = Date.newInstance(2018, 1, 1);
        Integer stepValue = 5;

        Test.startTest();
        FObject obj = new FObject(FObjectOne__c.SObjectType, numRecords)
            .setDefinition('$myTest1', 'serial type(date) from(2018-01-01) step(5)')
            .setDefinition(FObjectOne__c.TestDate__c, 'copy field($MYtest1)');
        List<FObjectOne__c> records = (List<FObjectOne__c>) obj.getRecords();
        Test.stopTest();

        Integer errors = 0;
        Date expectedResult = fromValue.addDays((numRecords - 1) * stepValue);
        Date actualResult = records[numRecords - 1].TestDate__c;

        if (expectedResult != actualResult) {
            errors++;
        }
        if (!FObject.processIsValid()) {
            errors++;
        }
        System.assertEquals(0, errors, 'virtual field - errors found');
    }

    @IsTest
    private static void serialDatetime_fromDatePositiveIntegerStep_expectSuccess() {
        Integer numRecords = 100;
        Datetime fromValue = Datetime.newInstance(2018, 1, 1);

        Test.startTest();
        FObject obj = new FObject(FObjectOne__c.SObjectType, numRecords);
        obj.setDefinition(FObjectOne__c.TestDatetime__c, 'serial type(datetime) from(2018-01-01) step(2)');
        List<FObjectOne__c> records = (List<FObjectOne__c>) obj.getRecords();
        Test.stopTest();

        Datetime expectedResult = fromValue.addDays((numRecords - 1) * 2);
        Datetime actualResult = records[numRecords - 1].TestDatetime__c;
        System.assertEquals(expectedResult, actualResult, 'serial datetime - errors found');
    }

    @IsTest
    private static void serialDatetime_fromDateNegativeIntegerStep_expectSuccess() {
        Integer numRecords = 100;
        Datetime fromValue = Datetime.newInstance(2018, 1, 1);

        Test.startTest();
        FObject obj = new FObject(FObjectOne__c.SObjectType, numRecords);
        obj.setDefinition(FObjectOne__c.TestDatetime__c, 'serial type(datetime) from(2018-01-01) step(-2)');
        List<FObjectOne__c> records = (List<FObjectOne__c>) obj.getRecords();
        Test.stopTest();

        Datetime expectedResult = fromValue.addDays((numRecords - 1) * (-2));
        Datetime actualResult = records[numRecords - 1].TestDatetime__c;
        System.assertEquals(expectedResult, actualResult, 'serial datetime - errors found');
    }

    @IsTest
    private static void serialDatetime_fromDatePositiveDecimalStep_expectSuccess() {
        Integer numRecords = 100;
        Datetime fromValue = Datetime.newInstance(2018, 1, 1);

        Test.startTest();
        FObject obj = new FObject(FObjectOne__c.SObjectType, numRecords);
        obj.setDefinition(FObjectOne__c.TestDatetime__c, 'serial type(datetime) from(2018-01-01) step(2.2)');
        List<FObjectOne__c> records = (List<FObjectOne__c>) obj.getRecords();
        Test.stopTest();

        Datetime expectedResult = fromValue.addDays((numRecords - 1) * 2);
        expectedResult = expectedResult.addHours((numRecords - 1) * 4);
        expectedResult = expectedResult.addMinutes((numRecords - 1) * 48);
        Datetime actualResult = records[numRecords - 1].TestDatetime__c;
        System.assertEquals(expectedResult, actualResult, 'serial datetime - errors found');
    }

    @IsTest
    private static void serialDatetime_fromDateNegativeDecimalStep_expectSuccess() {
        Integer numRecords = 100;
        Datetime fromValue = Datetime.newInstance(2018, 1, 1);

        Test.startTest();
        FObject obj = new FObject(FObjectOne__c.SObjectType, numRecords);
        obj.setDefinition(FObjectOne__c.TestDatetime__c, 'serial type(datetime) from(2018-01-01) step(-2.2)');
        List<FObjectOne__c> records = (List<FObjectOne__c>) obj.getRecords();
        Test.stopTest();

        Datetime expectedResult = fromValue.addDays((numRecords - 1) * (-2));
        expectedResult = expectedResult.addHours((numRecords - 1) * (-4));
        expectedResult = expectedResult.addMinutes((numRecords - 1) * (-48));
        Datetime actualResult = records[numRecords - 1].TestDatetime__c;
        System.assertEquals(expectedResult, actualResult, 'serial datetime - errors found');
    }

    @IsTest
    private static void serialDatetime_fromDatetimePositiveIntegerStep_expectSuccess() {
        Integer numRecords = 100;
        Datetime fromValue = Datetime.newInstance(2018, 1, 1, 2, 2, 2);

        Test.startTest();
        FObject obj = new FObject(FObjectOne__c.SObjectType, numRecords);
        obj.setDefinition(FObjectOne__c.TestDatetime__c, 'Serial Type(Datetime) From(2018-01-01 02:02:02) Step(2)');
        List<FObjectOne__c> records = (List<FObjectOne__c>) obj.getRecords();
        Test.stopTest();

        Datetime expectedResult = fromValue.addDays((numRecords - 1) * 2);
        Datetime actualResult = records[numRecords - 1].TestDatetime__c;
        System.assertEquals(expectedResult, actualResult, 'serial datetime - errors found');
    }

    @IsTest
    private static void serialDatetime_fromDatetimeNegativeIntegerStep_expectSuccess() {
        Integer numRecords = 100;
        Datetime fromValue = Datetime.newInstance(2018, 1, 1, 2, 2, 2);

        Test.startTest();
        FObject obj = new FObject(FObjectOne__c.SObjectType, numRecords);
        obj.setDefinition(FObjectOne__c.TestDatetime__c, 'Serial Type(Datetime) From(2018-01-01 02:02:02) Step(-2)');
        List<FObjectOne__c> records = (List<FObjectOne__c>) obj.getRecords();
        Test.stopTest();

        Datetime expectedResult = fromValue.addDays((numRecords - 1) * (-2));
        Datetime actualResult = records[numRecords - 1].TestDatetime__c;
        System.assertEquals(expectedResult, actualResult, 'serial datetime - errors found');
    }

    @IsTest
    private static void serialDatetime_fromDatetimePositiveDecimalStep_expectSuccess() {
        Integer numRecords = 100;
        Datetime fromValue = Datetime.newInstance(2018, 1, 1, 2, 2, 2);

        Test.startTest();
        FObject obj = new FObject(FObjectOne__c.SObjectType, numRecords);
        obj.setDefinition(FObjectOne__c.TestDatetime__c, 'Serial Type(Datetime) From(2018-01-01 02:02:02) Step(2.2)');
        List<FObjectOne__c> records = (List<FObjectOne__c>) obj.getRecords();
        Test.stopTest();

        Datetime expectedResult = fromValue.addDays((numRecords - 1) * 2);
        expectedResult = expectedResult.addHours((numRecords - 1) * 4);
        expectedResult = expectedResult.addMinutes((numRecords - 1) * 48);
        Datetime actualResult = records[numRecords - 1].TestDatetime__c;
        System.assertEquals(expectedResult, actualResult, 'serial datetime - errors found');
    }

    @IsTest
    private static void serialDatetime_fromDatetimeNegativeDecimalStep_expectSuccess() {
        Integer numRecords = 100;
        Datetime fromValue = Datetime.newInstance(2018, 1, 1, 2, 2, 2);

        Test.startTest();
        FObject obj = new FObject(FObjectOne__c.SObjectType, numRecords);
        obj.setDefinition(FObjectOne__c.TestDatetime__c, 'Serial Type(Datetime) From(2018-01-01 02:02:02) Step(-2.2)');
        List<FObjectOne__c> records = (List<FObjectOne__c>) obj.getRecords();
        Test.stopTest();

        Datetime expectedResult = fromValue.addDays((numRecords - 1) * (-2));
        expectedResult = expectedResult.addHours((numRecords - 1) * (-4));
        expectedResult = expectedResult.addMinutes((numRecords - 1) * (-48));
        Datetime actualResult = records[numRecords - 1].TestDatetime__c;
        System.assertEquals(expectedResult, actualResult, 'serial datetime - errors found');
    }

    @IsTest
    private static void serialDatetime_serializationInSameFObject_expectSuccess() {
        Integer numRecords = 10;

        Test.startTest();
        // create some serialized records
        FObject obj = new FObject(FObjectTwo__c.SObjectType, numRecords)
            .setDefinition(FObjectTwo__c.TestPicklistDRequired__c, 'random type(picklist)')
            .setDefinition(FObjectTwo__c.TestTextUnique__c,
                'serial type(datetime) from(2020-1-1 10:11:12) step(1)');
        List<FObjectOne__c> records = obj.getRecords();

        // create some more serialized records of the same FObject
        obj.setDefinition(FObjectTwo__c.TestText__c, 'static value(test)');
        records.addAll((List<FObjectOne__c>) obj.getRecords());

        // insert records
        String errorMessage = '';
        try {
            obj.insertRecords(true);
        } catch (Exception ex) {
            errorMessage = ex.getMessage();
        }
        Test.stopTest();

        Boolean result = FObject.processIsValid() && String.isBlank(errorMessage);
        System.assert(result, 'serial data - errors found');
    }

    @IsTest
    private static void serialDatetime_serializationInDifferentFObjects_expectSuccess() {
        Integer numRecords = 10;

        Test.startTest();
        // create some serialized records
        FObject obj1 = new FObject(FObjectTwo__c.SObjectType, numRecords)
            .setDefinition(FObjectTwo__c.TestPicklistDRequired__c, 'random type(picklist)')
            .setDefinition(FObjectTwo__c.TestTextUnique__c,
                'serial type(datetime) from(2020-1-1 10:11:12) step(1)');
        obj1.getRecords();

        // create some more serialized records of a different FObject
        FObject obj2 = new FObject(FObjectTwo__c.SObjectType, numRecords)
            .setDefinition(FObjectTwo__c.TestPicklistDRequired__c, 'random type(picklist)')
            .setDefinition(FObjectTwo__c.TestTextUnique__c,
                'serial type(datetime) from(2020-1-1 10:11:12) step(1)');
        obj2.getRecords();

        // insert records
        String errorMessage = '';
        try {
            obj1.insertRecords(true);
            obj2.insertRecords(true);
        } catch (Exception ex) {
            errorMessage = ex.getMessage();
        }
        Test.stopTest();

        Boolean result = FObject.processIsValid() && String.isBlank(errorMessage);
        System.assert(result, 'serial number - errors found');
    }

    @IsTest
    private static void serialDatetimeVirtualField_valid_expectSuccess() {
        Integer numRecords = 100;
        Datetime fromValue = Datetime.newInstance(2018, 1, 1);

        Test.startTest();
        FObject obj = new FObject(FObjectOne__c.SObjectType, numRecords)
            .setDefinition('$Test', 'serial type(datetime) from(2018-01-01) step(2)')
            .setDefinition(FObjectOne__c.TestDatetime__c, 'copy field($Test)');
        List<FObjectOne__c> records = (List<FObjectOne__c>) obj.getRecords();
        Test.stopTest();

        Datetime expectedResult = fromValue.addDays((numRecords - 1) * 2);
        Datetime actualResult = records[numRecords - 1].TestDatetime__c;
        System.assertEquals(expectedResult, actualResult, 'serial datetime - errors found');
    }

    @IsTest
    private static void serialList_cyclical_expectSuccess() {
        Test.startTest();
        FObject obj = new FObject(FObjectOne__c.SObjectType, 30)
            .setDefinition(FObjectOne__c.TestText__c, 'serial type(list) value(a1,a2,a3) mode(cyclical)');
        List<FObjectOne__c> records = (List<FObjectOne__c>) obj.getRecords();
        Test.stopTest();

        List<String> values = new List<String>{
            'a1', 'a2', 'a3'
        };

        Integer errors = 0;
        Integer counter = 0;
        for (FObjectOne__c objRecord : records) {
            if (objRecord.TestText__c != values.get(counter)) {
                errors++;
            }
            counter++;
            if (counter == values.size()) {
                counter = 0;
            }
        }
        if (!FObject.processIsValid()) {
            errors++;
        }
        System.assertEquals(0, errors, 'serial list - errors found');
    }

    @IsTest
    private static void serialList_cyclicalBlankMode_expectFailure() {
        FObject.setExceptionHandling('continue');

        Test.startTest();
        FObject obj = new FObject(FObjectOne__c.SObjectType, 30)
            .setDefinition(FObjectOne__c.TestText__c, 'serial type(list) value(a1,a2,a3) mode()')
            .createRecords();
        Test.stopTest();

        Integer errors = 0;
        if (obj.getErrors().get('TestText__c').isEmpty() ||
            !obj.getErrors().get('TestText__c').get(0).startsWith(ForceeaService.MESSAGE_INVALID_DEFINITION)) {
            errors += 1;
        }
        if (FObject.processIsValid()) {
            errors += 2;
        }
        System.assertEquals(0, errors, 'serial list - errors found');
    }

    @IsTest
    private static void serialList_cyclicalInvalidMode_expectFailure() {
        FObject.setExceptionHandling('continue');

        Test.startTest();
        FObject obj = new FObject(FObjectOne__c.SObjectType, 30)
            .setDefinition(FObjectOne__c.TestText__c, 'serial type(list) value(a1,a2,a3) mode(xyz)')
            .createRecords();
        Test.stopTest();

        Integer errors = 0;
        if (obj.getErrors().get('TestText__c').isEmpty() ||
            !obj.getErrors().get('TestText__c').get(0).startsWith(ForceeaService.MESSAGE_INVALID_DEFINITION)) {
            errors += 1;
        }
        if (FObject.processIsValid()) {
            errors += 2;
        }
        System.assertEquals(0, errors, 'serial list - errors found');
    }

    @IsTest
    private static void serialListVirtualField_cyclical_expectSuccess() {
        Test.startTest();
        FObject obj = new FObject(FObjectOne__c.SObjectType, 30)
            .setDefinition('$Test', 'serial type(list) value(a1,a2,a3) mode(cyclical)')
            .setDefinition(FObjectOne__c.TestText__c, 'copy field($Test)');
        List<FObjectOne__c> records = (List<FObjectOne__c>) obj.getRecords();
        Test.stopTest();

        List<String> values = new List<String>{
            'a1', 'a2', 'a3'
        };

        Integer errors = 0;
        Integer counter = 0;
        for (FObjectOne__c objRecord : records) {
            if (objRecord.TestText__c != values.get(counter)) {
                errors++;
            }
            counter++;
            if (counter == values.size()) {
                counter = 0;
            }
        }
        if (!FObject.processIsValid()) {
            errors++;
        }
        System.assertEquals(0, errors, 'serial list - errors found');
    }

    @IsTest
    private static void serialList_first_expectSuccess() {
        Test.startTest();
        FObject obj = new FObject(FObjectOne__c.SObjectType, 30)
            .setDefinition(FObjectOne__c.TestText__c, 'serial type(list) value(a1,a2,a3) mode(first)');
        List<FObjectOne__c> records = (List<FObjectOne__c>) obj.getRecords();
        Test.stopTest();

        List<String> values = new List<String>{
            'a1', 'a2', 'a3'
        };

        Integer errors = 0;
        Integer counter = 0;
        Integer index = 0;
        for (FObjectOne__c objRecord : records) {
            if (objRecord.TestText__c != values.get(index)) {
                errors++;
            }
            counter++;
            index++;
            if (counter >= values.size()) {
                index = 0;
            }
        }
        if (!FObject.processIsValid()) {
            errors++;
        }
        System.assertEquals(0, errors, 'serial list - errors found');
    }

    @IsTest
    private static void serialListVirtualField_first_expectSuccess() {
        Test.startTest();
        FObject obj = new FObject(FObjectOne__c.SObjectType, 30)
            .setDefinition('$Test', 'serial type(list) value(a1,a2,a3) mode(first)')
            .setDefinition(FObjectOne__c.TestText__c, 'copy field($Test)');
        List<FObjectOne__c> records = (List<FObjectOne__c>) obj.getRecords();
        Test.stopTest();

        List<String> values = new List<String>{
            'a1', 'a2', 'a3'
        };

        Integer errors = 0;
        Integer counter = 0;
        Integer index = 0;
        for (FObjectOne__c objRecord : records) {
            if (objRecord.TestText__c != values.get(index)) {
                errors++;
            }
            counter++;
            index++;
            if (counter >= values.size()) {
                index = 0;
            }
        }
        if (!FObject.processIsValid()) {
            errors++;
        }
        System.assertEquals(0, errors, 'serial list - errors found');
    }

    @IsTest
    private static void serialList_last_expectSuccess() {
        Test.startTest();
        FObject obj = new FObject(FObjectOne__c.SObjectType, 30)
            .setDefinition(FObjectOne__c.TestText__c, 'serial type(list) value(a1,a2,a3) mode(last)');
        List<FObjectOne__c> records = (List<FObjectOne__c>) obj.getRecords();
        Test.stopTest();

        List<String> values = new List<String>{
            'a1', 'a2', 'a3'
        };

        Integer errors = 0;
        Integer counter = 0;
        Integer index = 0;
        for (FObjectOne__c objRecord : records) {
            if (objRecord.TestText__c != values.get(index)) {
                errors++;
            }
            counter++;
            index++;
            if (counter >= values.size()) {
                index = values.size() - 1;
            }
        }
        if (!FObject.processIsValid()) {
            errors++;
        }
        System.assertEquals(0, errors, 'serial list - errors found');
    }

    @IsTest
    private static void serialListVirtualField_last_expectSuccess() {
        Test.startTest();
        FObject obj = new FObject(FObjectOne__c.SObjectType, 30)
            .setDefinition('$Test', 'serial type(list) value(a1,a2,a3) mode(last)')
            .setDefinition(FObjectOne__c.TestText__c, 'copy field($Test)');
        List<FObjectOne__c> records = (List<FObjectOne__c>) obj.getRecords();
        Test.stopTest();

        List<String> values = new List<String>{
            'a1', 'a2', 'a3'
        };

        Integer errors = 0;
        Integer counter = 0;
        Integer index = 0;
        for (FObjectOne__c objRecord : records) {
            if (objRecord.TestText__c != values.get(index)) {
                errors++;
            }
            counter++;
            index++;
            if (counter >= values.size()) {
                index = values.size() - 1;
            }
        }
        if (!FObject.processIsValid()) {
            errors++;
        }
        System.assertEquals(0, errors, 'serial list - errors found');
    }

    @IsTest
    private static void serialList_null_expectSuccess() {
        Test.startTest();
        FObject obj = new FObject(FObjectOne__c.SObjectType, 30)
            .setDefinition(FObjectOne__c.TestText__c, 'serial type(list) value(a1,a2,a3) mode(null)');
        List<FObjectOne__c> records = (List<FObjectOne__c>) obj.getRecords();
        Test.stopTest();

        List<String> values = new List<String>{
            'a1', 'a2', 'a3'
        };

        Integer errors = 0;
        Integer counter = 0;
        String str = '';
        for (FObjectOne__c objRecord : records) {
            if (counter < values.size()) {
                str = values.get(counter);
            } else {
                str = '';
            }
            if ((String.isNotBlank(objRecord.TestText__c) && objRecord.TestText__c != str) ||
                (String.isBlank(objRecord.TestText__c) && String.isNotBlank(str))) {
                errors++;
            }
            counter++;
        }
        if (!FObject.processIsValid()) {
            errors++;
        }
        System.assertEquals(0, errors, 'serial list - errors found');
    }

    @IsTest
    private static void serialListVirtualField_null_expectSuccess() {
        Test.startTest();
        FObject obj = new FObject(FObjectOne__c.SObjectType, 10)
            .setDefinition('$Test', 'serial type(list) value(a1,a2,a3) mode(null)')
            .setDefinition(FObjectOne__c.TestText__c, 'copy field($Test)');
        List<FObjectOne__c> records = (List<FObjectOne__c>) obj.getRecords();
        Test.stopTest();

        List<String> values = new List<String>{
            'a1', 'a2', 'a3'
        };

        Integer errors = 0;
        Integer counter = 0;
        String str = '';
        for (FObjectOne__c objRecord : records) {
            if (counter < values.size()) {
                str = values.get(counter);
            } else {
                str = '';
            }
            if ((String.isNotBlank(objRecord.TestText__c) && objRecord.TestText__c != str) ||
                (String.isBlank(objRecord.TestText__c) && String.isNotBlank(str))) {
                errors++;
            }
            counter++;
        }
        if (!FObject.processIsValid()) {
            errors++;
        }
        System.assertEquals(0, errors, 'serial list - errors found');
    }

    @IsTest
    private static void serialList_permutation_expectSuccess() {
        Test.startTest();
        FObject obj = new FObject(FObjectOne__c.SObjectType)
            .setNumberOfRecords('auto')
            .setDefinition(FObjectOne__c.TestText__c, 'serial type(list) value(a1,a2,a3) mode(permutation)')
            .setDefinition(FObjectOne__c.TestText__c, 'serial type(list) value(b1,b2,b3) mode(permutation)');
        List<FObjectOne__c> records = (List<FObjectOne__c>) obj.getRecords();
        Test.stopTest();

        List<String> values1 = new List<String>{
            'a1', 'a2', 'a3'
        };
        List<String> values2 = new List<String>{
            'b1', 'b2', 'b3'
        };

        Integer errors = 0;
        Integer counter1 = 0;
        Integer counter2 = 0;
        for (FObjectOne__c objRecord : records) {
            String str1 = values1.get(counter1);
            String str2 = values2.get(counter2);
            String str = str1 + str2;
            if (str != objRecord.TestText__c) {
                errors++;
            }

            counter2++;
            if (counter2 == values2.size()) {
                counter2 = 0;
                counter1++;
            }
        }
        if (!FObject.processIsValid()) {
            errors++;
        }
        System.assertEquals(0, errors, 'serial list - errors found');
    }

    @IsTest
    private static void serialListVirtualField_permutation_expectSuccess() {
        Test.startTest();
        FObject obj = new FObject(FObjectOne__c.SObjectType)
            .setNumberOfRecords('auto')
            .setDefinition('$Test', 'serial type(list) value(a1,a2,a3) mode(permutation)')
            .setDefinition('$Test', 'serial type(list) value(b1,b2,b3) mode(permutation)')
            .setDefinition(FObjectOne__c.TestText__c, 'copy field($Test)');
        List<FObjectOne__c> records = (List<FObjectOne__c>) obj.getRecords();
        Test.stopTest();

        List<String> values1 = new List<String>{
            'a1', 'a2', 'a3'
        };
        List<String> values2 = new List<String>{
            'b1', 'b2', 'b3'
        };

        Integer errors = 0;
        Integer counter1 = 0;
        Integer counter2 = 0;
        for (FObjectOne__c objRecord : records) {
            String str1 = values1.get(counter1);
            String str2 = values2.get(counter2);
            String str = str1 + str2;
            if (str != objRecord.TestText__c) {
                errors++;
            }

            counter2++;
            if (counter2 == values2.size()) {
                counter2 = 0;
                counter1++;
            }
        }
        if (!FObject.processIsValid()) {
            errors++;
        }
        System.assertEquals(0, errors, 'serial list - errors found');
    }

    @IsTest
    private static void serialNumberVirtualField_positiveIntegerStep_expectSuccess() {
        Integer numRecords = 201;
        Decimal fromValue = 10;
        Decimal stepValue = 2;

        Test.startTest();
        FObject obj = new FObject(FObjectOne__c.SObjectType)
            .setDefinition('$Integer', 'Serial Type(Number) From(10) Step(2) Scale(0)')
            .setDefinition(FObjectOne__c.TestInteger__c, 'copy field($Integer)');
        List<FObjectOne__c> records = (List<FObjectOne__c>) obj.getRecords();
        Test.stopTest();

        Decimal expectedResult = fromValue + (numRecords - 1) * stepValue;
        Decimal actualResult = records[numRecords - 1].TestInteger__c;
        System.assertEquals(expectedResult, actualResult, 'serial number - errors found');
    }

    @IsTest
    private static void serialNumber_negativeIntegerStep_expectSuccess() {
        Integer numRecords = 201;
        Decimal fromValue = 0;
        Decimal stepValue = -2;

        Test.startTest();
        FObject obj = new FObject(FObjectOne__c.SObjectType)
            .setDefinition(FObjectOne__c.TestInteger__c, 'Serial Type(Number) From(0) Step(-2) Scale(2)');
        List<FObjectOne__c> records = (List<FObjectOne__c>) obj.getRecords();
        Test.stopTest();

        Decimal expectedResult = fromValue + (numRecords - 1) * stepValue;
        Decimal actualResult = records[numRecords - 1].TestInteger__c;
        System.assertEquals(expectedResult, actualResult, 'serial number - errors found');
    }

    @IsTest
    private static void serialNumber_positiveDecimalStep_expectSuccess() {
        Integer numRecords = 201;
        Decimal fromValue = 10;
        Decimal stepValue = 1.52;

        Test.startTest();
        FObject obj = new FObject(FObjectOne__c.SObjectType, numRecords)
            .setDefinition(FObjectOne__c.TestDecimal__c, 'Serial Type(Number) From(10) Step(1.52) Scale(2)');
        List<FObjectOne__c> records = (List<FObjectOne__c>) obj.getRecords();
        Test.stopTest();

        Decimal expectedResult = fromValue + (numRecords - 1) * stepValue;
        Decimal actualResult = records[numRecords - 1].TestDecimal__c;
        System.assertEquals(expectedResult, actualResult, 'serial number - errors found');
    }

    @IsTest
    private static void serialNumber_negativeDecimalStep_expectSuccess() {
        Integer numRecords = 201;
        Decimal fromValue = 10;
        Decimal stepValue = -1.52;

        Test.startTest();
        FObject obj = new FObject(FObjectOne__c.SObjectType, numRecords);
        obj.setDefinition(FObjectOne__c.TestDecimal__c, 'Serial Type(Number) From(10) Step(-1.52) Scale(2)');
        List<FObjectOne__c> records = (List<FObjectOne__c>) obj.getRecords();
        Test.stopTest();

        Decimal expectedResult = fromValue + (numRecords - 1) * stepValue;
        Decimal actualResult = records[numRecords - 1].TestDecimal__c;
        System.assertEquals(expectedResult, actualResult, 'serial number - errors found');
    }

    @IsTest
    private static void serialNumber_serializationInSameFObject_expectSuccess() {
        Integer numRecords = 100;

        Test.startTest();
        //create some serialized records
        FObject obj = new FObject(FObjectOne__c.SObjectType, numRecords)
            .setDefinition(FObjectOne__c.TestTextRequired__c, 'static value(test)')
            .setDefinition(FObjectOne__c.TestIntegerUnique__c, 'serial type(number) from(1) step(1) scale(0)');
        List<FObjectOne__c> records = obj.getRecords();

        // create some more serialized records of the same FObject
        obj.setDefinition(FObjectOne__c.TestText__c, 'static value(test)');
        records.addAll((List<FObjectOne__c>) obj.getRecords());

        // insert records
        String errorMessage = '';
        try {
            obj.insertRecords(true);
        } catch (Exception ex) {
            errorMessage = ex.getMessage();
        }
        Test.stopTest();

        Boolean result = FObject.processIsValid() && String.isBlank(errorMessage);
        System.assert(result, 'serial number - errors found');
    }

    @IsTest
    private static void serialNumber_serializationInDifferentFObjects_expectSuccess() {
        Integer numRecords = 100;

        Test.startTest();
        //create some serialized records
        FObject obj1 = new FObject(FObjectOne__c.SObjectType, numRecords)
            .setDefinition(FObjectOne__c.TestTextRequired__c, 'static value(test)')
            .setDefinition(FObjectOne__c.TestIntegerUnique__c, 'serial type(number) from(1) step(1) scale(0)');
        obj1.getRecords();

        // create some more serialized records of a different FObject
        FObject obj2 = new FObject(FObjectOne__c.SObjectType, numRecords)
            .setDefinition(FObjectOne__c.TestTextRequired__c, 'static value(test)')
            .setDefinition(FObjectOne__c.TestIntegerUnique__c, 'serial type(number) from(1) step(1) scale(0)');
        obj2.getRecords();

        // insert records
        String errorMessage = '';
        try {
            obj1.insertRecords(true);
            obj2.insertRecords(true);
        } catch (Exception ex) {
            errorMessage = ex.getMessage();
        }
        Test.stopTest();

        Boolean result = FObject.processIsValid() && String.isBlank(errorMessage);
        System.assert(result, 'serial number - errors found');
    }

    @IsTest
    private static void serialNumber_positiveIntegerStep_expectSuccess() {
        Integer numRecords = 201;
        Decimal fromValue = 10;
        Decimal stepValue = 2;

        Test.startTest();
        FObject obj = new FObject(FObjectOne__c.SObjectType)
            .setDefinition(FObjectOne__c.TestInteger__c, 'Serial Type(Number) From(10) Step(2) Scale(0)');
        List<FObjectOne__c> records = (List<FObjectOne__c>) obj.getRecords();
        Test.stopTest();

        Decimal expectedResult = fromValue + (numRecords - 1) * stepValue;
        Decimal actualResult = records[numRecords - 1].TestInteger__c;
        System.assertEquals(expectedResult, actualResult, 'serial number - errors found');
    }

    @IsTest
    private static void serialNumberMode_negativeIntegerStep1_expectSuccess() {
        FObject obj = new FObject(FObjectOne__c.SObjectType, 100)
            .setDefinition(FObjectOne__c.TestDecimal__c,
                'serial type(number) from(10) to(100) step(-5) scale(2) mode(cyclical)');

        Test.startTest();
        List<Decimal> numbers = getDecimalNumbers(obj.getRecords());
        Test.stopTest();

        System.assert(numbers.isEmpty(), 'serial number mode - errors found');
    }

    @IsTest
    private static void serialNumberMode_negativeIntegerStep2_expectSuccess() {
        FObject obj = new FObject(FObjectOne__c.SObjectType, 100)
            .setDefinition(FObjectOne__c.TestDecimal__c,
                'serial type(number) from(100) to(10) step(-5) scale(2) mode(cyclical)');

        Test.startTest();
        List<Decimal> numbers = getDecimalNumbers(obj.getRecords());
        Test.stopTest();

        System.assert(serialNumbersAreCorrect(numbers, 100, 10, -5), 'serial number mode - errors found');
    }

    @IsTest
    private static void serialNumberMode_positiveIntegerStep1_expectSuccess() {
        FObject obj = new FObject(FObjectOne__c.SObjectType, 100)
            .setDefinition(FObjectOne__c.TestDecimal__c,
                'serial type(number) from(10) to(100) step(5) scale(2) mode(cyclical)');

        Test.startTest();
        List<Decimal> numbers = getDecimalNumbers(obj.getRecords());
        Test.stopTest();

        System.assert(serialNumbersAreCorrect(numbers, 10, 100, 5), 'serial number mode - errors found');
    }

    @IsTest
    private static void serialNumberMode_positiveIntegerStep2_expectSuccess() {
        FObject obj = new FObject(FObjectOne__c.SObjectType, 100)
            .setDefinition(FObjectOne__c.TestDecimal__c,
                'serial type(number) from(100) to(10) step(5) scale(2) mode(cyclical)');

        Test.startTest();
        List<Decimal> numbers = getDecimalNumbers(obj.getRecords());
        Test.stopTest();

        System.assert(numbers.isEmpty(), 'serial number mode - errors found');
    }

    @IsTest
    private static void serialNumberMode_sameFromTo_expectSuccess() {
        FObject obj = new FObject(FObjectOne__c.SObjectType, 100)
            .setDefinition(FObjectOne__c.TestDecimal__c,
                'serial type(number) from(10) to(10) step(-5) scale(2) mode(cyclical)');

        Test.startTest();
        Set<Decimal> numbers = getDecimalNumbersSet(obj.getRecords());
        Test.stopTest();

        System.assertEquals(1, numbers.size(), 'serial number mode - errors found');
    }

    @IsTest
    private static void serialNumberMode_modePermutation_expectSuccess() {
        FObject obj = new FObject(FObjectOne__c.SObjectType, 100)
            .setNumberOfRecords('auto')
            .setDefinition(FObjectOne__c.TestDecimal__c,
                'serial type(number) from(10) to(20) step(2.5) scale(2) mode(permutation)');

        Test.startTest();
        List<Decimal> numbers = getDecimalNumbers(obj.getRecords());
        Test.stopTest();

        System.assertEquals(5, numbers.size(), 'serial number mode - errors found');
        System.assertEquals(5, obj.getRecords().size(), 'serial number mode - errors found');
    }

    @IsTest
    private static void serialNumberMode_modeFirst_expectSuccess() {
        FObject obj = new FObject(FObjectOne__c.SObjectType, 100)
            .setDefinition(FObjectOne__c.TestDecimal__c,
                'serial type(number) from(10) to(20) step(2.5) scale(2) mode(first)');

        Test.startTest();
        Set<Decimal> numbers = getDecimalNumbersSet(obj.getRecords());
        Test.stopTest();

        System.assertEquals(5, numbers.size(), 'serial number mode - errors found');
    }

    @IsTest
    private static void serialNumberMode_modeLast_expectSuccess() {
        FObject obj = new FObject(FObjectOne__c.SObjectType, 100)
            .setDefinition(FObjectOne__c.TestDecimal__c,
                'serial type(number) from(10) to(20) step(2.5) scale(2) mode(last)');

        Test.startTest();
        Set<Decimal> numbers = getDecimalNumbersSet(obj.getRecords());
        Test.stopTest();

        System.assertEquals(5, numbers.size(), 'serial number mode - errors found');
    }

    @IsTest
    private static void serialNumberMode_modeNull_expectSuccess() {
        FObject obj = new FObject(FObjectOne__c.SObjectType, 100)
            .setDefinition(FObjectOne__c.TestDecimal__c,
                'serial type(number) from(10) to(20) step(2.5) scale(2) mode(null)');

        Test.startTest();
        List<Decimal> numbers = getDecimalNumbers(obj.getRecords());
        Test.stopTest();

        System.assert(serialNumbersAreCorrect(numbers, 10, 20, 2.5), 'serial number mode - errors found');
        System.assertEquals(5, numbers.size(), 'serial number mode - errors found');
    }

    @IsTest
    private static void serialPicklist_cyclical_expectSuccess() {
        Test.startTest();
        FObject obj = new FObject(FObjectOne__c.SObjectType, 30)
            .setDefinition(FObjectOne__c.TestPicklistA__c, 'serial type(picklist) mode(cyclical)');
        List<FObjectOne__c> records = (List<FObjectOne__c>) obj.getRecords();
        Test.stopTest();

        List<String> values = new List<String>{
            'a0', 'a1', 'a2', 'a3'
        };

        Integer errors = 0;
        Integer counter = 0;
        for (FObjectOne__c objRecord : records) {
            if (objRecord.TestPicklistA__c != values.get(counter)) {
                errors++;
            }
            counter++;
            if (counter == values.size()) {
                counter = 0;
            }
        }
        if (!FObject.processIsValid()) {
            errors++;
        }
        System.assertEquals(0, errors, 'serial picklist - errors found');
    }

    @IsTest
    private static void serialPicklistRecordType_cyclical_expectSuccess() {
        Test.startTest();
        FObject obj = new FObject(FObjectOne__c.SObjectType, 30)
            .setDefinition(FObjectOne__c.RecordTypeId, 'serial type(picklist) mode(cyclical)');
        List<FObjectOne__c> records = (List<FObjectOne__c>) obj.getRecords();
        Test.stopTest();

        List<String> recordTypeIds = new List<String>();
        for (RecordType rt : [
            SELECT Id
            FROM RecordType
            WHERE SobjectType = 'FObjectOne__c'
            ORDER BY DeveloperName
        ]) {
            recordTypeIds.add(rt.Id);
        }

        Integer errors = 0;
        Integer counter = 0;
        for (FObjectOne__c objRecord : records) {
            if (objRecord.RecordTypeId != recordTypeIds.get(counter)) {
                errors++;
            }
            counter++;
            if (counter == recordTypeIds.size()) {
                counter = 0;
            }
        }
        if (!FObject.processIsValid()) {
            errors++;
        }
        System.assertEquals(0, errors, 'serial picklist - errors found');
    }

    @IsTest
    private static void serialPicklistVirtualField_cyclical_expectFailure() {
        FObject.setExceptionHandling('continue');

        Test.startTest();
        FObject obj = new FObject(FObjectOne__c.SObjectType)
            .setDefinition('$Picklist', 'serial type(picklist) mode(cyclical)')
            .setDefinition(FObjectOne__c.TestPicklistA__c, 'copy field($Picklist)');
        obj.getRecords();
        Test.stopTest();

        Boolean result = !FObject.processIsValid();
        System.assert(result, 'serial picklist - errors found');
    }

    @IsTest
    private static void serialPicklist_first_expectSuccess() {
        Test.startTest();
        FObject obj = new FObject(FObjectOne__c.SObjectType, 30)
            .setDefinition(FObjectOne__c.TestPicklistA__c, 'serial type(picklist) mode(first)');
        List<FObjectOne__c> records = (List<FObjectOne__c>) obj.getRecords();
        Test.stopTest();

        List<String> values = new List<String>{
            'a0', 'a1', 'a2', 'a3'
        };

        Integer errors = 0;
        Integer counter = 0;
        Integer index = 0;
        for (FObjectOne__c objRecord : records) {
            if (objRecord.TestPicklistA__c != values.get(index)) {
                errors++;
            }
            counter++;
            index++;
            if (counter >= values.size()) {
                index = 0;
            }
        }
        System.assertEquals(0, errors, 'serial picklist - errors found');
    }

    @IsTest
    private static void serialPicklistRecordType_first_expectSuccess() {
        Test.startTest();
        FObject obj = new FObject(FObjectOne__c.SObjectType, 30)
            .setDefinition(FObjectOne__c.RecordTypeId, 'serial type(picklist) mode(first)');
        List<FObjectOne__c> records = (List<FObjectOne__c>) obj.getRecords();
        Test.stopTest();

        List<String> recordTypeIds = new List<String>();
        for (RecordType rt : [
            SELECT Id
            FROM RecordType
            WHERE SobjectType = 'FObjectOne__c'
            ORDER BY DeveloperName
        ]) {
            recordTypeIds.add(rt.Id);
        }

        Integer errors = 0;
        Integer counter = 0;
        Integer index = 0;
        for (FObjectOne__c objRecord : records) {
            if (objRecord.RecordTypeId != recordTypeIds.get(index)) {
                errors++;
            }
            counter++;
            index++;
            if (counter >= recordTypeIds.size()) {
                index = 0;
            }
        }
        System.assertEquals(0, errors, 'serial picklist - errors found');
    }

    @IsTest
    private static void serialPicklistVirtualField_first_expectFailure() {
        FObject.setExceptionHandling('continue');

        Test.startTest();
        FObject obj = new FObject(FObjectOne__c.SObjectType)
            .setDefinition('$Picklist', 'serial type(picklist) mode(first)')
            .setDefinition(FObjectOne__c.TestPicklistA__c, 'copy field($Picklist)');
        obj.getRecords();
        Test.stopTest();

        Boolean result = !FObject.processIsValid();
        System.assert(result, 'serial picklist - errors found');
    }

    @IsTest
    private static void serialPicklist_last_expectSuccess() {
        Test.startTest();
        FObject obj = new FObject(FObjectOne__c.SObjectType, 30)
            .setDefinition(FObjectOne__c.TestPicklistA__c, 'serial type(picklist) mode(last)');
        List<FObjectOne__c> records = (List<FObjectOne__c>) obj.getRecords();
        Test.stopTest();

        List<String> values = new List<String>{
            'a0', 'a1', 'a2', 'a3'
        };

        Integer errors = 0;
        Integer counter = 0;
        Integer index = 0;
        for (FObjectOne__c objRecord : records) {
            if (objRecord.TestPicklistA__c != values.get(index)) {
                errors++;
            }
            counter++;
            index++;
            if (counter >= values.size()) {
                index = values.size() - 1;
            }
        }
        System.assertEquals(0, errors, 'serial picklist - errors found');
    }

    @IsTest
    private static void serialPicklistRecordType_last_expectSuccess() {
        Test.startTest();
        FObject obj = new FObject(FObjectOne__c.SObjectType, 30)
            .setDefinition(FObjectOne__c.RecordTypeId, 'serial type(picklist) mode(last)');
        List<FObjectOne__c> records = (List<FObjectOne__c>) obj.getRecords();
        Test.stopTest();

        List<String> recordTypeIds = new List<String>();
        for (RecordType rt : [
            SELECT Id
            FROM RecordType
            WHERE SobjectType = 'FObjectOne__c'
            ORDER BY DeveloperName
        ]) {
            recordTypeIds.add(rt.Id);
        }

        Integer errors = 0;
        Integer counter = 0;
        Integer index = 0;
        for (FObjectOne__c objRecord : records) {
            if (objRecord.RecordTypeId != recordTypeIds.get(index)) {
                errors++;
            }
            counter++;
            index++;
            if (counter >= recordTypeIds.size()) {
                index = recordTypeIds.size() - 1;
            }
        }
        System.assertEquals(0, errors, 'serial picklist - errors found');
    }

    @IsTest
    private static void serialPicklistVirtualField_last_expectFailure() {
        FObject.setExceptionHandling('continue');

        Test.startTest();
        FObject obj = new FObject(FObjectOne__c.SObjectType)
            .setDefinition('$Picklist', 'serial type(picklist) mode(last)')
            .setDefinition(FObjectOne__c.TestPicklistA__c, 'copy field($Picklist)');
        obj.getRecords();
        Test.stopTest();

        Boolean result = !FObject.processIsValid();
        System.assert(result, 'serial picklist - errors found');
    }

    @IsTest
    private static void serialPicklist_null_expectSuccess() {
        Test.startTest();
        FObject obj = new FObject(FObjectOne__c.SObjectType, 30)
            .setDefinition(FObjectOne__c.TestPicklistA__c, 'serial type(picklist) mode(null)');
        List<FObjectOne__c> records = (List<FObjectOne__c>) obj.getRecords();
        Test.stopTest();

        List<String> values = new List<String>{
            'a0', 'a1', 'a2', 'a3'
        };

        Integer errors = 0;
        Integer counter = 0;
        String str = '';
        for (FObjectOne__c objRecord : records) {
            if (counter < values.size()) {
                str = values.get(counter);
            } else {
                str = '';
            }
            if ((String.isNotBlank(objRecord.TestPicklistA__c) && objRecord.TestPicklistA__c != str) ||
                (String.isBlank(objRecord.TestPicklistA__c) && String.isNotBlank(str))) {
                errors++;
            }
            counter++;
        }
        if (!FObject.processIsValid()) {
            errors++;
        }
        System.assertEquals(0, errors, 'serial picklist - errors found');
    }

    @IsTest
    private static void serialPicklistRecordType_null_expectSuccess() {
        Test.startTest();
        FObject obj = new FObject(FObjectOne__c.SObjectType, 30)
            .setDefinition(FObjectOne__c.RecordTypeId, 'serial type(picklist) mode(null)');
        List<FObjectOne__c> records = (List<FObjectOne__c>) obj.getRecords();
        Test.stopTest();

        List<String> recordTypeIds = new List<String>();
        for (RecordType rt : [
            SELECT Id
            FROM RecordType
            WHERE SobjectType = 'FObjectOne__c'
            ORDER BY DeveloperName
        ]) {
            recordTypeIds.add(rt.Id);
        }

        Integer errors = 0;
        Integer counter = 0;
        String str = '';
        for (FObjectOne__c objRecord : records) {
            if (counter < recordTypeIds.size()) {
                str = recordTypeIds.get(counter);
            } else {
                str = '';
            }
            if ((String.isNotBlank(objRecord.RecordTypeId) && objRecord.RecordTypeId != str) ||
                (String.isBlank(objRecord.RecordTypeId) && String.isNotBlank(str))) {
                errors++;
            }
            counter++;
        }
        if (!FObject.processIsValid()) {
            errors++;
        }
        System.assertEquals(0, errors, 'serial picklist - errors found');
    }

    @IsTest
    private static void serialPicklistVirtualField_null_expectFailure() {
        FObject.setExceptionHandling('continue');

        Test.startTest();
        FObject obj = new FObject(FObjectOne__c.SObjectType)
            .setDefinition('$Picklist', 'serial type(picklist) mode(null)')
            .setDefinition(FObjectOne__c.TestPicklistA__c, 'copy field($Picklist)');
        obj.getRecords();
        Test.stopTest();

        Boolean result = !FObject.processIsValid();
        System.assert(result, 'serial picklist - errors found');
    }

    @IsTest
    private static void serialPicklist_permutation_expectSuccess() {
        Test.startTest();
        FObject obj = new FObject(FObjectOne__c.SObjectType)
            .setNumberOfRecords('auto')
            .setDefinition(FObjectOne__c.TestPicklistA__c, 'serial type(picklist) mode(permutation)')
            .setDefinition(FObjectOne__c.TestText__c, 'serial type(list) value(b1,b2,b3) mode(permutation)');
        List<FObjectOne__c> records = (List<FObjectOne__c>) obj.getRecords();
        Test.stopTest();

        List<String> values1 = new List<String>{
            'a0', 'a1', 'a2', 'a3'
        };
        List<String> values2 = new List<String>{
            'b1', 'b2', 'b3'
        };

        Integer errors = 0;
        Integer counter1 = 0;
        Integer counter2 = 0;
        for (FObjectOne__c objRecord : records) {
            String str1 = values1.get(counter1);
            String str2 = values2.get(counter2);
            String str = str1 + str2;
            if (str != (objRecord.TestPicklistA__c + objRecord.TestText__c)) {
                errors++;
            }

            counter2++;
            if (counter2 == values2.size()) {
                counter2 = 0;
                counter1++;
            }
        }
        if (!FObject.processIsValid()) {
            errors++;
        }
        System.assertEquals(0, errors, 'serial list - errors found');
    }

    @IsTest
    private static void serialPicklistRecordType_permutation_expectSuccess() {
        Test.startTest();
        FObject obj = new FObject(FObjectOne__c.SObjectType)
            .setNumberOfRecords('auto')
            .setDefinition(FObjectOne__c.RecordTypeId, 'serial type(picklist) mode(permutation)')
            .setDefinition(FObjectOne__c.TestText__c, 'serial type(list) value(b1,b2,b3) mode(permutation)');
        List<FObjectOne__c> records = (List<FObjectOne__c>) obj.getRecords();
        Test.stopTest();

        List<String> recordTypeIds = new List<String>();
        for (RecordType rt : [
            SELECT Id
            FROM RecordType
            WHERE SobjectType = 'FObjectOne__c'
            ORDER BY DeveloperName
        ]) {
            recordTypeIds.add(rt.Id);
        }
        List<String> values2 = new List<String>{
            'b1', 'b2', 'b3'
        };

        Integer errors = 0;
        Integer counter1 = 0;
        Integer counter2 = 0;
        for (FObjectOne__c objRecord : records) {
            String str1 = recordTypeIds.get(counter1);
            String str2 = values2.get(counter2);
            String str = str1 + str2;
            if (str != (objRecord.RecordTypeId + objRecord.TestText__c)) {
                errors++;
            }

            counter2++;
            if (counter2 == values2.size()) {
                counter2 = 0;
                counter1++;
            }
        }
        if (!FObject.processIsValid()) {
            errors++;
        }
        System.assertEquals(0, errors, 'serial list - errors found');
    }

    @IsTest
    private static void serialPicklistVirtualField_permutation_expectFailure() {
        FObject.setExceptionHandling('continue');

        Test.startTest();
        FObject obj = new FObject(FObjectOne__c.SObjectType)
            .setDefinition('$Picklist', 'serial type(picklist) mode(permutation)')
            .setDefinition(FObjectOne__c.TestPicklistA__c, 'copy field($Picklist)');
        obj.getRecords();
        Test.stopTest();

        Boolean result = !FObject.processIsValid();
        System.assert(result, 'serial picklist - errors found');
    }

    @IsTest
    private static void serialPicklistExcept_cyclical_expectSuccess() {
        Test.startTest();
        FObject obj = new FObject(FObjectOne__c.SObjectType, 30)
            .setDefinition(FObjectOne__c.TestPicklistA__c, 'serial type(picklist) except(A1,a2) mode(cyclical)');
        List<FObjectOne__c> records = (List<FObjectOne__c>) obj.getRecords();
        Test.stopTest();

        List<String> values = new List<String>{
            'a0', 'a3'
        };

        Integer errors = 0;
        Integer counter = 0;
        for (FObjectOne__c objRecord : records) {
            if (objRecord.TestPicklistA__c != values.get(counter)) {
                errors++;
            }
            counter++;
            if (counter == values.size()) {
                counter = 0;
            }
        }
        if (!FObject.processIsValid()) {
            errors++;
        }
        System.assertEquals(0, errors, 'serial picklist - errors found');
    }

    @IsTest
    private static void serialPicklistRecordTypeExcept_cyclical_expectSuccess() {
        Test.startTest();
        FObject obj = new FObject(FObjectOne__c.SObjectType, 30)
            .setDefinition(FObjectOne__c.RecordTypeId, 'serial type(picklist) except(recordtypea) mode(cyclical)');
        List<FObjectOne__c> records = (List<FObjectOne__c>) obj.getRecords();
        Test.stopTest();

        List<String> recordTypeIds = new List<String>();
        for (RecordType rt : [
            SELECT Id
            FROM RecordType
            WHERE SobjectType = 'FObjectOne__c'
                AND DeveloperName != 'RecordTypeA'
            ORDER BY DeveloperName
        ]) {
            recordTypeIds.add(rt.Id);
        }

        Integer errors = 0;
        Integer counter = 0;
        for (FObjectOne__c objRecord : records) {
            if (objRecord.RecordTypeId != recordTypeIds.get(counter)) {
                errors++;
            }
            counter++;
            if (counter == recordTypeIds.size()) {
                counter = 0;
            }
        }
        if (!FObject.processIsValid()) {
            errors++;
        }
        System.assertEquals(0, errors, 'serial picklist - errors found');
    }

    @IsTest
    private static void serialPicklistExcept_first_expectSuccess() {
        Test.startTest();
        FObject obj = new FObject(FObjectOne__c.SObjectType, 30)
            .setDefinition(FObjectOne__c.TestPicklistA__c, 'serial type(picklist) except(A1,a2) mode(first)');
        List<FObjectOne__c> records = (List<FObjectOne__c>) obj.getRecords();
        Test.stopTest();

        List<String> values = new List<String>{
            'a0', 'a3'
        };

        Integer errors = 0;
        Integer counter = 0;
        Integer index = 0;
        for (FObjectOne__c objRecord : records) {
            if (objRecord.TestPicklistA__c != values.get(index)) {
                errors++;
            }
            counter++;
            index++;
            if (counter >= values.size()) {
                index = 0;
            }
        }
        System.assertEquals(0, errors, 'serial picklist - errors found');
    }

    @IsTest
    private static void serialPicklistRecordTypeExcept_first_expectSuccess() {
        Test.startTest();
        FObject obj = new FObject(FObjectOne__c.SObjectType, 30)
            .setDefinition(FObjectOne__c.RecordTypeId, 'serial type(picklist) except(recordtypea) mode(first)');
        List<FObjectOne__c> records = (List<FObjectOne__c>) obj.getRecords();
        Test.stopTest();

        List<String> recordTypeIds = new List<String>();
        for (RecordType rt : [
            SELECT Id
            FROM RecordType
            WHERE SobjectType = 'FObjectOne__c'
                AND DeveloperName != 'RecordTypeA'
            ORDER BY DeveloperName
        ]) {
            recordTypeIds.add(rt.Id);
        }

        Integer errors = 0;
        Integer counter = 0;
        Integer index = 0;
        for (FObjectOne__c objRecord : records) {
            if (objRecord.RecordTypeId != recordTypeIds.get(index)) {
                errors++;
            }
            counter++;
            index++;
            if (counter >= recordTypeIds.size()) {
                index = 0;
            }
        }
        System.assertEquals(0, errors, 'serial picklist - errors found');
    }

    @IsTest
    private static void serialPicklistExcept_last_expectSuccess() {
        Test.startTest();
        FObject obj = new FObject(FObjectOne__c.SObjectType, 30)
            .setDefinition(FObjectOne__c.TestPicklistA__c, 'serial type(picklist) except(A1,a2) mode(last)');
        List<FObjectOne__c> records = (List<FObjectOne__c>) obj.getRecords();
        Test.stopTest();

        List<String> values = new List<String>{
            'a0', 'a3'
        };

        Integer errors = 0;
        Integer counter = 0;
        Integer index = 0;
        for (FObjectOne__c objRecord : records) {
            if (objRecord.TestPicklistA__c != values.get(index)) {
                errors++;
            }
            counter++;
            index++;
            if (counter >= values.size()) {
                index = values.size() - 1;
            }
        }
        System.assertEquals(0, errors, 'serial picklist - errors found');
    }

    @IsTest
    private static void serialPicklistRecordTypeExcept_last_expectSuccess() {
        Test.startTest();
        FObject obj = new FObject(FObjectOne__c.SObjectType, 30)
            .setDefinition(FObjectOne__c.RecordTypeId, 'serial type(picklist) except(recordtypea) mode(last)');
        List<FObjectOne__c> records = (List<FObjectOne__c>) obj.getRecords();
        Test.stopTest();

        List<String> recordTypeIds = new List<String>();
        for (RecordType rt : [
            SELECT Id
            FROM RecordType
            WHERE SobjectType = 'FObjectOne__c'
                AND DeveloperName != 'RecordTypeA'
            ORDER BY DeveloperName
        ]) {
            recordTypeIds.add(rt.Id);
        }

        Integer errors = 0;
        Integer counter = 0;
        Integer index = 0;
        for (FObjectOne__c objRecord : records) {
            if (objRecord.RecordTypeId != recordTypeIds.get(index)) {
                errors++;
            }
            counter++;
            index++;
            if (counter >= recordTypeIds.size()) {
                index = recordTypeIds.size() - 1;
            }
        }
        System.assertEquals(0, errors, 'serial picklist - errors found');
    }

    @IsTest
    private static void serialPicklistExcept_null_expectSuccess() {
        Test.startTest();
        FObject obj = new FObject(FObjectOne__c.SObjectType, 30)
            .setDefinition(FObjectOne__c.TestPicklistA__c, 'serial type(picklist) except(A1,a2) mode(null)');
        List<FObjectOne__c> records = (List<FObjectOne__c>) obj.getRecords();
        Test.stopTest();

        List<String> values = new List<String>{
            'a0', 'a3'
        };

        Integer errors = 0;
        Integer counter = 0;
        String str = '';
        for (FObjectOne__c objRecord : records) {
            if (counter < values.size()) {
                str = values.get(counter);
            } else {
                str = '';
            }
            if ((String.isNotBlank(objRecord.TestPicklistA__c) && objRecord.TestPicklistA__c != str) ||
                (String.isBlank(objRecord.TestPicklistA__c) && String.isNotBlank(str))) {
                errors++;
            }
            counter++;
        }
        if (!FObject.processIsValid()) {
            errors++;
        }
        System.assertEquals(0, errors, 'serial picklist - errors found');
    }

    @IsTest
    private static void serialPicklistRecordTypeExcept_null_expectSuccess() {
        Test.startTest();
        FObject obj = new FObject(FObjectOne__c.SObjectType, 30)
            .setDefinition(FObjectOne__c.RecordTypeId, 'serial type(picklist) except(recordtypea) mode(null)');
        List<FObjectOne__c> records = (List<FObjectOne__c>) obj.getRecords();
        Test.stopTest();

        List<String> recordTypeIds = new List<String>();
        for (RecordType rt : [
            SELECT Id
            FROM RecordType
            WHERE SobjectType = 'FObjectOne__c'
                AND DeveloperName != 'RecordTypeA'
            ORDER BY DeveloperName
        ]) {
            recordTypeIds.add(rt.Id);
        }

        Integer errors = 0;
        Integer counter = 0;
        String str = '';
        for (FObjectOne__c objRecord : records) {
            if (counter < recordTypeIds.size()) {
                str = recordTypeIds.get(counter);
            } else {
                str = '';
            }
            if ((String.isNotBlank(objRecord.RecordTypeId) && objRecord.RecordTypeId != str) ||
                (String.isBlank(objRecord.RecordTypeId) && String.isNotBlank(str))) {
                errors++;
            }
            counter++;
        }
        if (!FObject.processIsValid()) {
            errors++;
        }
        System.assertEquals(0, errors, 'serial picklist - errors found');
    }

    @IsTest
    private static void serialPicklistExcept_permutation_expectSuccess() {
        Test.startTest();
        FObject obj = new FObject(FObjectOne__c.SObjectType)
            .setNumberOfRecords('auto')
            .setDefinition(FObjectOne__c.TestPicklistA__c, 'serial type(picklist) except(A1,a2) mode(permutation)')
            .setDefinition(FObjectOne__c.TestText__c, 'serial type(list) value(b1,b2,b3) mode(permutation)');
        List<FObjectOne__c> records = (List<FObjectOne__c>) obj.getRecords();
        Test.stopTest();

        List<String> values1 = new List<String>{
            'a0', 'a3'
        };
        List<String> values2 = new List<String>{
            'b1', 'b2', 'b3'
        };

        Integer errors = 0;
        Integer counter1 = 0;
        Integer counter2 = 0;
        for (FObjectOne__c objRecord : records) {
            String str1 = values1.get(counter1);
            String str2 = values2.get(counter2);
            String str = str1 + str2;
            if (str != (objRecord.TestPicklistA__c + objRecord.TestText__c)) {
                errors++;
            }

            counter2++;
            if (counter2 == values2.size()) {
                counter2 = 0;
                counter1++;
            }
        }
        if (!FObject.processIsValid()) {
            errors++;
        }
        System.assertEquals(0, errors, 'serial list - errors found');
    }

    @IsTest
    private static void serialPicklistRecordTypeExcept_permutation_expectSuccess() {
        Test.startTest();
        FObject obj = new FObject(FObjectOne__c.SObjectType)
            .setNumberOfRecords('auto')
            .setDefinition(FObjectOne__c.RecordTypeId, 'serial type(picklist) except(recordtypea) mode(permutation)')
            .setDefinition(FObjectOne__c.TestText__c, 'serial type(list) value(b1,b2,b3) mode(permutation)');
        List<FObjectOne__c> records = (List<FObjectOne__c>) obj.getRecords();
        Test.stopTest();

        List<String> recordTypeIds = new List<String>();
        for (RecordType rt : [
            SELECT Id
            FROM RecordType
            WHERE SobjectType = 'FObjectOne__c'
                AND DeveloperName != 'RecordTypeA'
            ORDER BY DeveloperName
        ]) {
            recordTypeIds.add(rt.Id);
        }
        List<String> values2 = new List<String>{
            'b1', 'b2', 'b3'
        };

        Integer errors = 0;
        Integer counter1 = 0;
        Integer counter2 = 0;
        for (FObjectOne__c objRecord : records) {
            String str1 = recordTypeIds.get(counter1);
            String str2 = values2.get(counter2);
            String str = str1 + str2;
            if (str != (objRecord.RecordTypeId + objRecord.TestText__c)) {
                errors++;
            }

            counter2++;
            if (counter2 == values2.size()) {
                counter2 = 0;
                counter1++;
            }
        }
        if (!FObject.processIsValid()) {
            errors++;
        }
        System.assertEquals(0, errors, 'serial list - errors found');
    }

    /* UTILITY METHODS */

    private static Set<Decimal> getDecimalNumbersSet(List<SObject> records) {
        return new Set<Decimal>(getDecimalNumbers(records));
    }

    private static List<Decimal> getDecimalNumbers(List<SObject> records) {
        List<Decimal> results = new List<Decimal>();

        for (SObject record : records) {
            if (record.get('TestDecimal__c') == null) {
                continue;
            }
            Decimal dec = (Decimal) record.get('TestDecimal__c');
            results.add(dec);
        }

        return results;
    }

    private static Boolean serialNumbersAreCorrect(List<Decimal> numbers,
        Decimal startNumber, Decimal finishNumber, Decimal stepNumber) {
        Boolean result = true;

        Integer listCounter = 0;
        for (Decimal serialNumber = startNumber; serialNumber <= finishNumber; serialNumber += stepNumber) {
            if (numbers.get(listCounter) != serialNumber) {
                result = false;
                break;
            }
            listCounter++;
        }

        return result;
    }

    @IsTest
    private static void itShouldInsertRtWithoutExceptions() {
        // GIVEN A template with FObjectOne__c and FObjectTwo__c
        String rtDevName2 = 'RecordTypeA';
        Forceea.Template template = getTemplateWithoutExceptions(rtDevName2);

        // WHEN We insert the template
        Test.startTest();
        template.insertRecords(true);
        Test.stopTest();

        // THEN The inserted records have the correct record type
        ForceeaUtilityTest.checkIfRt1IsAnyValid(template);
        ForceeaUtilityTest.checkIfRt2IsValid(template, rtDevName2);
    }

    @IsTest
    private static void itShouldInsertRtWithExceptions() {
        // GIVEN A template with FObjectOne__c and FObjectTwo__c
        String rtDevName2 = 'RecordTypeA';
        Forceea.Template template = getTemplateWithExceptions(rtDevName2);

        // WHEN We insert the template
        Test.startTest();
        template.insertRecords(true);
        Test.stopTest();

        // THEN The inserted records have the correct record type
        ForceeaUtilityTest.checkIfRt1IsAnyValid(template, new Set<String> { 'RecordTypeA', 'RecordTypeB' });
        ForceeaUtilityTest.checkIfRt2IsValid(template, rtDevName2);
    }

    /* HELPER METHODS */

    private static Forceea.Template getTemplateWithoutExceptions(String rtDevName2) {
        Forceea.Template result = ForceeaUtilityTest.getTemplate('rtDevName1', rtDevName2);

        result.getFObject(ForceeaUtilityTest.FOBJECT1)
            .setDefinition(FObjectOne__c.RecordTypeId, 'serial type(picklist) mode(cyclical)');
        result.getFObject(ForceeaUtilityTest.FOBJECT2)
            .setDefinition(FObjectTwo__c.RecordTypeId, 'static value({@rtDevName2})');

        return result;
    }

    private static Forceea.Template getTemplateWithExceptions(String rtDevName2) {
        Forceea.Template result = ForceeaUtilityTest.getTemplate('rtDevName1', rtDevName2);

        result.getFObject(ForceeaUtilityTest.FOBJECT1)
            .setDefinition(FObjectOne__c.RecordTypeId, 'serial type(picklist) except(RecordTypeC) mode(cyclical)');
        result.getFObject(ForceeaUtilityTest.FOBJECT2)
            .setDefinition(FObjectTwo__c.RecordTypeId, 'static value({@rtDevName2})');

        return result;
    }

}