package com.gregtechceu.gtceu.api.recipe.ingredient;

import com.gregtechceu.gtceu.GTCEu;
import com.gregtechceu.gtceu.api.GTValues;
import com.gregtechceu.gtceu.api.capability.recipe.FluidRecipeCapability;
import com.gregtechceu.gtceu.api.capability.recipe.IO;
import com.gregtechceu.gtceu.api.capability.recipe.ItemRecipeCapability;
import com.gregtechceu.gtceu.api.machine.MetaMachine;
import com.gregtechceu.gtceu.api.machine.SimpleTieredMachine;
import com.gregtechceu.gtceu.api.machine.multiblock.WorkableElectricMultiblockMachine;
import com.gregtechceu.gtceu.api.machine.multiblock.WorkableMultiblockMachine;
import com.gregtechceu.gtceu.api.machine.trait.notifiable.NotifiableFluidTank;
import com.gregtechceu.gtceu.api.machine.trait.notifiable.NotifiableItemStackHandler;
import com.gregtechceu.gtceu.api.recipe.GTRecipeType;
import com.gregtechceu.gtceu.common.data.GTMaterials;
import com.gregtechceu.gtceu.common.data.GTRecipeTypes;
import com.gregtechceu.gtceu.common.machine.multiblock.part.FluidHatchPartMachine;
import com.gregtechceu.gtceu.common.machine.multiblock.part.ItemBusPartMachine;
import com.gregtechceu.gtceu.common.machine.multiblock.part.ParallelHatchPartMachine;
import com.gregtechceu.gtceu.gametest.util.TestUtils;

import net.minecraft.core.BlockPos;
import net.minecraft.gametest.framework.BeforeBatch;
import net.minecraft.gametest.framework.GameTest;
import net.minecraft.gametest.framework.GameTestHelper;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.util.valueproviders.UniformInt;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.Items;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraftforge.fluids.FluidStack;
import net.minecraftforge.gametest.GameTestHolder;
import net.minecraftforge.gametest.PrefixGameTestTemplate;

/**
 * Test cases:
 * Do many passes of most tests as a safeguard against bad rolls
 * Same output more than once
 * Out of bounds
 * Output a multiple of batchparallels
 * Rolls of 0
 * Forced rolls of 0 breaking recipes
 */
@PrefixGameTestTemplate(false)
@GameTestHolder(GTCEu.MOD_ID)
public class IntProviderFluidIngredientTest {

    private static GTRecipeType CR_RECIPE_TYPE;
    private static GTRecipeType LCR_RECIPE_TYPE;
    private static GTRecipeType CENTRIFUGE_RECIPE_TYPE;

    // fluids used in recipes. Up top here for quick replacements.
    private static final FluidStack CR_IN = GTMaterials.Hydrogen.getFluid(1);
    private static final FluidStack CR_OUT = GTMaterials.Iron.getFluid(1);
    private static final FluidStack CR_TICK_IN = GTMaterials.Silicon.getFluid(1);
    private static final FluidStack CR_TICK_OUT = GTMaterials.Cobalt.getFluid(1);
    private static final FluidStack LCR_IN = GTMaterials.Oxygen.getFluid(1);
    private static final FluidStack LCR_OUT = GTMaterials.Copper.getFluid(1);
    private static final FluidStack LCENT_IN = GTMaterials.Nitrogen.getFluid(1);
    private static final FluidStack LCENT_OUT = GTMaterials.Gold.getFluid(1);
    private static final FluidStack RUBBER = GTMaterials.Rubber.getFluid(1);
    private static final FluidStack REDSTONE = GTMaterials.Redstone.getFluid(1);
    private static final ItemStack COBBLE = new ItemStack(Items.COBBLESTONE);
    private static final ItemStack STONE = new ItemStack(Items.STONE);

    /**
     * How many times to repeat the Batch and Parallel random roll tests to avoid false positives
     * Currently set to 7, with singleblock recipes processing up to 9 fluids, allowing for stacks of up to 63 fluids.
     * Set 20 times for Batch/Parallel multis
     */
    private static final int REPLICAS = 7;
    private static final int MULTI_REPLICAS = 20;

    @BeforeBatch(batch = "RangedFluidIngredients")
    public static void prepare(ServerLevel level) {
        CR_RECIPE_TYPE = TestUtils.createRecipeType("ranged_fluid_ingredient_cr_tests", GTRecipeTypes.CHEMICAL_RECIPES);
        LCR_RECIPE_TYPE = TestUtils.createRecipeType("ranged_fluid_ingredient_lcr_tests",
                GTRecipeTypes.LARGE_CHEMICAL_RECIPES);
        CENTRIFUGE_RECIPE_TYPE = TestUtils.createRecipeType("ranged_fluid_ingredient_centrifuge_tests",
                GTRecipeTypes.CENTRIFUGE_RECIPES);

        var CRHandler = CR_RECIPE_TYPE.getAdditionHandler();
        CRHandler.beginStaging();
        var LCRHandler = LCR_RECIPE_TYPE.getAdditionHandler();
        LCRHandler.beginStaging();
        var centHandler = CENTRIFUGE_RECIPE_TYPE.getAdditionHandler();
        centHandler.beginStaging();

        CRHandler.addStaging(CR_RECIPE_TYPE
                .recipeBuilder("test_ranged_input_fluid_cr")
                .inputFluidsRanged(CR_IN, UniformInt.of(0, 9))
                .inputItems(COBBLE)
                .outputFluids(REDSTONE)
                .EUt(GTValues.V[GTValues.HV])
                .duration(2)
                .buildRawRecipe());

        CRHandler.addStaging(CR_RECIPE_TYPE
                .recipeBuilder("test_ranged_output_fluid_cr")
                .inputFluids(CR_OUT)
                .outputFluidsRanged(REDSTONE, UniformInt.of(0, 9))
                .EUt(GTValues.V[GTValues.HV])
                .duration(2)
                .buildRawRecipe());

        LCRHandler.addStaging(LCR_RECIPE_TYPE
                .recipeBuilder("test_ranged_input_fluid_lcr")
                .inputFluidsRanged(LCR_IN, UniformInt.of(0, 9))
                .inputFluids(RUBBER)
                .outputFluids(REDSTONE)
                .EUt(GTValues.V[GTValues.HV])
                .duration(2)
                .buildRawRecipe());

        LCRHandler.addStaging(LCR_RECIPE_TYPE
                .recipeBuilder("test_ranged_output_fluid_lcr")
                .inputFluids(LCR_OUT)
                .outputFluidsRanged(REDSTONE, UniformInt.of(0, 9))
                .EUt(GTValues.V[GTValues.HV])
                .duration(2)
                .buildRawRecipe());

        centHandler.addStaging(CENTRIFUGE_RECIPE_TYPE
                .recipeBuilder("test_ranged_input_fluid_cent")
                .inputFluidsRanged(LCENT_IN, UniformInt.of(0, 40))
                .inputItems(COBBLE)
                .outputFluids(REDSTONE)
                .EUt(GTValues.V[GTValues.IV])
                .duration(4)
                .buildRawRecipe());

        centHandler.addStaging(CENTRIFUGE_RECIPE_TYPE
                .recipeBuilder("test_ranged_output_fluid_cent")
                .inputFluids(LCENT_OUT)
                .outputFluidsRanged(REDSTONE, UniformInt.of(0, 40))
                .EUt(GTValues.V[GTValues.IV])
                .duration(4)
                .buildRawRecipe());

        CRHandler.addStaging(CR_RECIPE_TYPE
                .recipeBuilder("test_ranged_tick_input_fluid_cr")
                .perTick(true)
                .inputFluidsRanged(CR_TICK_IN, UniformInt.of(0, 9))
                .perTick(false)
                .inputItems(COBBLE)
                .outputItems(STONE)
                .EUt(GTValues.V[GTValues.HV])
                .duration(7)
                .buildRawRecipe());

        CRHandler.addStaging(CR_RECIPE_TYPE
                .recipeBuilder("test_ranged_tick_output_fluid_cr")
                .inputItems(STONE)
                .perTick(true)
                .outputFluidsRanged(CR_TICK_OUT, UniformInt.of(0, 9))
                .perTick(false)
                .EUt(GTValues.V[GTValues.HV])
                .duration(7)
                .buildRawRecipe());

        centHandler.addStaging(CENTRIFUGE_RECIPE_TYPE
                .recipeBuilder("test_ranged_tick_input_fluid_cent")
                .perTick(true)
                .inputFluidsRanged(CR_TICK_IN, UniformInt.of(0, 9))
                .perTick(false)
                .inputItems(COBBLE)
                .outputItems(STONE)
                .EUt(GTValues.V[GTValues.IV])
                .duration(4)
                .buildRawRecipe());

        centHandler.addStaging(CENTRIFUGE_RECIPE_TYPE
                .recipeBuilder("test_ranged_tick_output_fluid_cent")
                .inputFluids(CR_TICK_OUT)
                .perTick(true)
                .outputFluidsRanged(CR_TICK_OUT, UniformInt.of(0, 9))
                .perTick(false)
                .EUt(GTValues.V[GTValues.IV])
                .duration(4)
                .buildRawRecipe());

        CRHandler.completeStaging();
        LCRHandler.completeStaging();
        centHandler.completeStaging();
    }

    private static MetaMachine getMetaMachine(BlockEntity entity) {
        return (MetaMachine) entity;
    }

    private record BusHolder(ItemBusPartMachine inputBus1, FluidHatchPartMachine inputHatch1,
                             ItemBusPartMachine outputBus1,
                             FluidHatchPartMachine outputHatch1, WorkableMultiblockMachine controller) {}

    private record BusHolderBatchParallel(ItemBusPartMachine inputBus1, FluidHatchPartMachine inputHatch1,
                                          ItemBusPartMachine outputBus1,
                                          FluidHatchPartMachine outputHatch1,
                                          WorkableElectricMultiblockMachine controller,
                                          ParallelHatchPartMachine parallelHatch) {}

    /**
     * Retrieves the busses for this LCR template and force a multiblock structure check
     *
     * @param helper the GameTestHelper
     * @return the busses, in the BusHolder record.
     */
    private static BusHolder getBussesAndFormLCR(GameTestHelper helper) {
        WorkableMultiblockMachine controller = (WorkableMultiblockMachine) getMetaMachine(
                helper.getBlockEntity(new BlockPos(1, 2, 0)));
        TestUtils.formMultiblock(helper, controller);
        controller.setRecipeType(LCR_RECIPE_TYPE);
        ItemBusPartMachine inputBus1 = (ItemBusPartMachine) getMetaMachine(
                helper.getBlockEntity(new BlockPos(2, 1, 0)));
        FluidHatchPartMachine inputHatch1 = (FluidHatchPartMachine) getMetaMachine(
                helper.getBlockEntity(new BlockPos(2, 2, 0)));
        ItemBusPartMachine outputBus1 = (ItemBusPartMachine) getMetaMachine(
                helper.getBlockEntity(new BlockPos(0, 1, 0)));
        FluidHatchPartMachine outputHatch1 = (FluidHatchPartMachine) getMetaMachine(
                helper.getBlockEntity(new BlockPos(0, 2, 0)));
        return new BusHolder(inputBus1, inputHatch1, outputBus1, outputHatch1, controller);
    }

    /**
     * Retrieves the busses for this Large Centrifuge template and force a multiblock structure check
     *
     * @param helper the GameTestHelper
     * @return the busses, in the BusHolder record.
     */
    private static BusHolderBatchParallel getBussesAndFormLCENT(GameTestHelper helper) {
        WorkableElectricMultiblockMachine controller = (WorkableElectricMultiblockMachine) getMetaMachine(
                helper.getBlockEntity(new BlockPos(2, 2, 0)));
        TestUtils.formMultiblock(helper, controller);
        controller.setRecipeType(CENTRIFUGE_RECIPE_TYPE);
        ItemBusPartMachine inputBus1 = (ItemBusPartMachine) getMetaMachine(
                helper.getBlockEntity(new BlockPos(1, 2, 0)));
        FluidHatchPartMachine inputHatch1 = (FluidHatchPartMachine) getMetaMachine(
                helper.getBlockEntity(new BlockPos(0, 2, 0)));
        ItemBusPartMachine outputBus1 = (ItemBusPartMachine) getMetaMachine(
                helper.getBlockEntity(new BlockPos(2, 1, 0)));
        FluidHatchPartMachine outputHatch1 = (FluidHatchPartMachine) getMetaMachine(
                helper.getBlockEntity(new BlockPos(1, 1, 0)));
        ParallelHatchPartMachine parallelHatch = (ParallelHatchPartMachine) getMetaMachine(
                helper.getBlockEntity(new BlockPos(3, 3, 0)));
        return new BusHolderBatchParallel(inputBus1, inputHatch1, outputBus1, outputHatch1, controller, parallelHatch);
    }

    // test for IntProviderFluidIngredient.test()
    @GameTest(template = "empty", batch = "RangedFluidIngredients")
    public static void rangedFluidIngredientTestEqualTest(GameTestHelper helper) {
        var ingredient = IntProviderFluidIngredient.of(GTMaterials.Water.getFluid(1), 1, 5);
        helper.assertTrue(ingredient.test(GTMaterials.Water.getFluid(3)),
                "IntProviderFluidIngredient.test doesn't match when it should have");
        // This should work since test only tries the fluid type.
        helper.assertTrue(ingredient.test(GTMaterials.Water.getFluid(64)),
                "IntProviderFluidIngredient.test doesn't match when it should have with value outside bounds");
        helper.assertFalse(ingredient.test(GTMaterials.Lava.getFluid(3)),
                "IntProviderFluidIngredient.test shouldn't match with different fluids");
        helper.succeed();
    }

    // test for IntProviderFluidIngredient.getStacks()
    @GameTest(template = "empty", batch = "RangedFluidIngredients")
    public static void rangedFluidIngredientGetStacksTest(GameTestHelper helper) {
        var ingredient = IntProviderFluidIngredient.of(GTMaterials.Water.getFluid(1), 1, 500000);

        // This will print a "Cannot get stacks" warning to the log. Ignore it.
        GTCEu.LOGGER.warn("This test will warn that it cannot get stacks. This is supposed to happen.");
        helper.assertTrue(ingredient.getStacks().length == 0,
                "A ranged fluid ingredient should not return fluids!");
        GTCEu.LOGGER.warn("If you are reading this line it means the test passed.");

        ingredient.rollSampledCount();
        var stacks = ingredient.collapse().getStacks();
        helper.assertTrue(stacks.length == 1,
                "Replaced IntProviderFluidIngredient should only return 1 fluid when made with 1 fluid");
        helper.assertTrue(stacks[0].isFluidEqual(GTMaterials.Water.getFluid(1)),
                "Replaced IntProviderFluidIngredient should have fluid equal to what it was made with");

        helper.succeed();
    }

    // Failure Test for singleblock machine with ranged fluid input
    // Provides too little input fluid, should not run recipes.
    @GameTest(template = "singleblock_charged_cr", batch = "RangedFluidIngredients")
    public static void singleblockRangedFluidInputFailure(GameTestHelper helper) {
        SimpleTieredMachine machine = (SimpleTieredMachine) getMetaMachine(
                helper.getBlockEntity(new BlockPos(0, 1, 0)));

        machine.setRecipeType(CR_RECIPE_TYPE);
        NotifiableItemStackHandler itemIn = (NotifiableItemStackHandler) machine
                .getCapabilitiesFlat(IO.IN, ItemRecipeCapability.CAP).get(0);
        NotifiableFluidTank fluidIn = (NotifiableFluidTank) machine
                .getCapabilitiesFlat(IO.IN, FluidRecipeCapability.CAP).get(0);
        NotifiableFluidTank fluidOut = (NotifiableFluidTank) machine
                .getCapabilitiesFlat(IO.OUT, FluidRecipeCapability.CAP).get(0);

        int runs = 10;
        fluidIn.setFluidInTank(0, new FluidStack(CR_IN, 8));
        itemIn.setStackInSlot(1, COBBLE.copyWithCount(runs));
        // 1t to turn on, 2t per non-recipe run
        helper.runAfterDelay(runs * 2 + 1, () -> {
            FluidStack results = fluidIn.getFluidInTank(0);

            helper.assertTrue(fluidOut.isEmpty(),
                    "Singleblock CR should not have run, ran [" +
                            fluidOut.getFluidInTank(0).getAmount() + "] times");
            helper.assertTrue(TestUtils.isFluidStackEqual(results, new FluidStack(CR_IN, 8)),
                    "Singleblock CR should not have consumed items, consumed [" +
                            (8 - results.getAmount()) + "]");

            helper.succeed();
        });
    }

    // Test for output preroll on singleblock machine with ranged fluid output
    @GameTest(template = "singleblock_charged_cr", batch = "RangedFluidIngredients")
    public static void singleblockRangedFluidOutputPreroll(GameTestHelper helper) {
        SimpleTieredMachine machine = (SimpleTieredMachine) getMetaMachine(
                helper.getBlockEntity(new BlockPos(0, 1, 0)));

        machine.setRecipeType(CR_RECIPE_TYPE);
        NotifiableFluidTank fluidIn = (NotifiableFluidTank) machine
                .getCapabilitiesFlat(IO.IN, FluidRecipeCapability.CAP).get(0);
        NotifiableFluidTank fluidOut = (NotifiableFluidTank) machine
                .getCapabilitiesFlat(IO.OUT, FluidRecipeCapability.CAP).get(0);

        fluidIn.setFluidInTank(0, new FluidStack(CR_OUT, REPLICAS));
        // 1t to turn on, 2t per recipe run
        // get the result of each preroll independently
        for (int i = 0; i < REPLICAS; i++) {
            final int finalI = i; // lambda preserve you
            helper.runAfterDelay(2 * i + 1, () -> {
                helper.assertFalse(machine.recipeLogic.getLastRecipe() == null,
                        "Singleblock fluid CR Preroll was not running a recipe when preroll was checked!");
                var outputPrerolls = machine.recipeLogic.getLastRecipe().outputs.get(FluidRecipeCapability.CAP);
                helper.assertFalse(outputPrerolls.size() == 0,
                        "Singleblock fluid CR Preroll's recipe output contained no fluids!");
                helper.assertFalse(outputPrerolls.get(0).content() instanceof IRangedIngredient,
                        "Singleblock fluid CR Preroll's recipe failed to preroll and replace its " +
                                "ranged ingredient!");
            });
        }
        TestUtils.succeedAfterTest(helper);
    }

    // Test for singleblock machine with ranged fluid input
    @GameTest(template = "singleblock_charged_cr", batch = "RangedFluidIngredients")
    public static void singleblockRangedFluidInput(GameTestHelper helper) {
        SimpleTieredMachine machine = (SimpleTieredMachine) getMetaMachine(
                helper.getBlockEntity(new BlockPos(0, 1, 0)));

        machine.setRecipeType(CR_RECIPE_TYPE);
        NotifiableItemStackHandler itemIn = (NotifiableItemStackHandler) machine
                .getCapabilitiesFlat(IO.IN, ItemRecipeCapability.CAP).get(0);
        NotifiableFluidTank fluidIn = (NotifiableFluidTank) machine
                .getCapabilitiesFlat(IO.IN, FluidRecipeCapability.CAP).get(0);
        NotifiableFluidTank fluidOut = (NotifiableFluidTank) machine
                .getCapabilitiesFlat(IO.OUT, FluidRecipeCapability.CAP).get(0);

        fluidIn.setFluidInTank(0, new FluidStack(CR_IN, 64));
        itemIn.setStackInSlot(0, COBBLE.copyWithCount(REPLICAS));
        // 1t to turn on, 2t per recipe run
        // get the result of each roll independently
        int[] addedRolls = new int[REPLICAS];
        for (int i = 0; i < REPLICAS; i++) {
            final int finalI = i; // lambda preserve you
            helper.runAfterDelay(2 * i + 1, () -> {
                addedRolls[finalI] = fluidIn.getFluidInTank(0).getAmount();
            });
        }
        // check the results of all rolls together
        helper.runAfterDelay(REPLICAS * 2 + 2, () -> {
            FluidStack results = fluidIn.getFluidInTank(0);
            int upperLimit = 64 - (REPLICAS * 0);
            int lowerLimit = 64 - (REPLICAS * 9);
            helper.assertTrue(
                    TestUtils.isFluidStackEqual(fluidOut.getFluidInTank(0), new FluidStack(REDSTONE, REPLICAS)),
                    "Singleblock CR didn't complete correct number of recipes, completed [" +
                            fluidOut.getFluidInTank(0).getAmount() + "] not [" + REPLICAS + "]");
            helper.assertTrue(TestUtils.isFluidWithinRange(results, lowerLimit, upperLimit),
                    "Singleblock CR didn't consume correct number of fluids, consumed [" +
                            (64 - results.getAmount()) + "] not [" + lowerLimit + "-" + upperLimit + "]");
            helper.assertFalse((results.getAmount() == lowerLimit),
                    "Singleblock CR rolled max value on every roll");
            helper.assertFalse((results.getAmount() == upperLimit),
                    "Singleblock CR rolled min value on every roll");

            // check if all the rolls were equal, but not min/max
            int[] rolls = new int[REPLICAS];
            rolls[0] = 64 - addedRolls[0];
            boolean allEqual = false;
            for (int i = 1; i < REPLICAS; i++) {
                rolls[i] = addedRolls[i - 1] - addedRolls[i];
                if (rolls[i] == rolls[i - 1]) {
                    allEqual = true;
                } else {
                    allEqual = false;
                    break;
                }
            }
            helper.assertFalse(allEqual,
                    "Singleblock CR rolled the same value on every input roll (rolled " + rolls[0] + ")");
            helper.succeed();
        });
    }

    // Test for singleblock machine with ranged fluid output
    @GameTest(template = "singleblock_charged_cr", batch = "RangedFluidIngredients")
    public static void singleblockRangedFluidOutput(GameTestHelper helper) {
        SimpleTieredMachine machine = (SimpleTieredMachine) getMetaMachine(
                helper.getBlockEntity(new BlockPos(0, 1, 0)));

        machine.setRecipeType(CR_RECIPE_TYPE);
        NotifiableFluidTank fluidIn = (NotifiableFluidTank) machine
                .getCapabilitiesFlat(IO.IN, FluidRecipeCapability.CAP).get(0);
        NotifiableFluidTank fluidOut = (NotifiableFluidTank) machine
                .getCapabilitiesFlat(IO.OUT, FluidRecipeCapability.CAP).get(0);

        fluidIn.setFluidInTank(0, new FluidStack(CR_OUT, REPLICAS));
        // 1t to turn on, 2t per recipe run
        // get the result of each roll independently
        int[] addedRolls = new int[REPLICAS];
        for (int i = 0; i < REPLICAS; i++) {
            final int finalI = i; // lambda preserve you
            helper.runAfterDelay(2 * i + 3, () -> {
                addedRolls[finalI] = fluidOut.getFluidInTank(0).getAmount();
            });
        }
        // check the results of all rolls together
        helper.runAfterDelay(REPLICAS * 2 + 1, () -> {
            helper.assertTrue(fluidIn.getFluidInTank(0).isEmpty(),
                    "Singleblock CR didn't complete correct number of recipes, completed [" +
                            fluidIn.getFluidInTank(0).getAmount() + "] not [" + REPLICAS + "]");
            FluidStack results = fluidOut.getFluidInTank(0);
            helper.assertTrue(TestUtils.isFluidWithinRange(results, REPLICAS, REPLICAS * 9),
                    "Singleblock CR didn't produce correct number of fluids, produced [" +
                            results.getAmount() + "] not [" + REPLICAS + "-" + (REPLICAS * 9) + "]");
            helper.assertFalse((results.getAmount() == REPLICAS * 9),
                    "Singleblock CR rolled max value on every roll");
            helper.assertFalse((results.getAmount() == REPLICAS * 0),
                    "Singleblock CR rolled min value on every roll");

            // check if all the rolls were equal, but not min/max
            int[] rolls = new int[REPLICAS];
            rolls[0] = addedRolls[0];
            boolean allEqual = false;
            for (int i = 1; i < REPLICAS; i++) {
                rolls[i] = addedRolls[i] - addedRolls[i - 1];
                if (rolls[i] == rolls[i - 1]) {
                    allEqual = true;
                } else {
                    allEqual = false;
                    break;
                }
            }
            helper.assertFalse(allEqual,
                    "Singleblock CR rolled the same value on every input roll (rolled " + rolls[0] + ")");
            helper.succeed();
        });
    }

    // test for multiblock machine with ranged fluid input
    @GameTest(template = "lcr_ranged_ingredients",
              batch = "RangedFluidIngredients")
    public static void multiblockLCRRangedFluidInput(GameTestHelper helper) {
        BusHolder busHolder = getBussesAndFormLCR(helper);

        NotifiableFluidTank fluidIn = busHolder.inputHatch1.tank;
        NotifiableFluidTank fluidOut = busHolder.outputHatch1.tank;

        fluidIn.setFluidInTank(0, new FluidStack(LCR_IN, 64));
        fluidIn.setFluidInTank(1, new FluidStack(RUBBER, REPLICAS));
        // 1t to turn on, 2t per recipe run
        // get the result of each roll independently
        int[] addedRolls = new int[REPLICAS];
        for (int i = 0; i < REPLICAS; i++) {
            final int finalI = i; // lambda preserve you
            helper.runAfterDelay(2 * i + 1, () -> {
                addedRolls[finalI] = fluidIn.getFluidInTank(0).getAmount();
            });
        }
        // check the results of all rolls together
        helper.runAfterDelay(REPLICAS * 2 + 2, () -> {
            FluidStack results = fluidIn.getFluidInTank(0);
            int upperLimit = 64 - (REPLICAS * 0);
            int lowerLimit = 64 - (REPLICAS * 9);
            helper.assertTrue(
                    TestUtils.isFluidStackEqual(fluidOut.getFluidInTank(0), new FluidStack(REDSTONE, REPLICAS)),
                    "LCR didn't complete correct number of recipes, completed [" +
                            fluidOut.getFluidInTank(0).getAmount() + "] not [" + REPLICAS + "]");
            helper.assertTrue(TestUtils.isFluidWithinRange(results, lowerLimit, upperLimit),
                    "LCR didn't consume correct number of fluids, consumed [" +
                            (64 - results.getAmount()) + "] not [" + lowerLimit + "-" + upperLimit + "]");
            helper.assertFalse((results.getAmount() == lowerLimit),
                    "LCR rolled max value on every roll");
            helper.assertFalse((results.getAmount() == upperLimit),
                    "LCR rolled min value on every roll");

            // check if all the rolls were equal, but not min/max
            int[] rolls = new int[REPLICAS];
            rolls[0] = 64 - addedRolls[0];
            boolean allEqual = false;
            for (int i = 1; i < REPLICAS; i++) {
                rolls[i] = addedRolls[i - 1] - addedRolls[i];
                if (rolls[i] == rolls[i - 1]) {
                    allEqual = true;
                } else {
                    allEqual = false;
                    break;
                }
            }
            helper.assertFalse(allEqual,
                    "LCR rolled the same value on every input roll (rolled " + rolls[0] + ")");
            helper.succeed();
        });
    }

    // test for multiblock machine with ranged fluid input
    @GameTest(template = "lcr_ranged_ingredients",
              batch = "RangedFluidIngredients")
    public static void multiblockLCRRangedFluidOutput(GameTestHelper helper) {
        BusHolder busHolder = getBussesAndFormLCR(helper);

        final NotifiableFluidTank fluidIn = busHolder.inputHatch1.tank;
        final NotifiableFluidTank fluidOut = busHolder.outputHatch1.tank;

        fluidIn.setFluidInTank(0, new FluidStack(LCR_OUT, REPLICAS));
        // 1t to turn on, 2t per recipe run
        // get the result of each roll independently
        int[] addedRolls = new int[REPLICAS];
        for (int i = 0; i < REPLICAS; i++) {
            final int finalI = i; // lambda preserve you
            helper.runAfterDelay(2 * i + 3, () -> {
                addedRolls[finalI] = fluidOut.getFluidInTank(0).getAmount();
            });
        }
        // check the results of all rolls together
        helper.runAfterDelay(REPLICAS * 2 + 1, () -> {
            helper.assertTrue(fluidIn.getFluidInTank(0).isEmpty(),
                    "LCR didn't complete correct number of recipes, completed [" +
                            fluidIn.getFluidInTank(0).getAmount() + "] not [" + REPLICAS + "]");
            FluidStack results = fluidOut.getFluidInTank(0);
            helper.assertTrue(TestUtils.isFluidWithinRange(results, REPLICAS, REPLICAS * 9),
                    "LCR didn't produce correct number of fluids, produced [" +
                            results.getAmount() + "] not [" + REPLICAS + "-" + (REPLICAS * 9) + "]");
            helper.assertFalse((results.getAmount() == REPLICAS * 9),
                    "LCR rolled max value on every roll");
            helper.assertFalse((results.getAmount() == REPLICAS * 0),
                    "LCR rolled min value on every roll");

            // check if all the rolls were equal, but not min/max
            int[] rolls = new int[REPLICAS];
            rolls[0] = addedRolls[0];
            boolean allEqual = false;
            for (int i = 1; i < REPLICAS; i++) {
                rolls[i] = addedRolls[i] - addedRolls[i - 1];
                if (rolls[i] == rolls[i - 1]) {
                    allEqual = true;
                } else {
                    allEqual = false;
                    break;
                }
            }
            helper.assertFalse(allEqual,
                    "LCR rolled the same value on every input roll (rolled " + rolls[0] + ")");
            helper.succeed();
        });
    }

    // test for multiblock machine with 16x Parallels with ranged fluid input
    @GameTest(template = "large_centrifuge_zpm_batch_parallel16",
              batch = "RangedFluidIngredients",
              timeoutTicks = 2000)
    public static void multiblockLCentRangedFluidInput16Parallel(GameTestHelper helper) {
        BusHolderBatchParallel busHolder = getBussesAndFormLCENT(helper);

        NotifiableItemStackHandler itemIn = busHolder.inputBus1.getInventory();
        final NotifiableFluidTank fluidIn = busHolder.inputHatch1.tank;
        final NotifiableFluidTank fluidOut = busHolder.outputHatch1.tank;

        int batches = 1; // unused on this test
        int parallels = 16;
        final int amount = 40 * batches * parallels;
        busHolder.controller.setBatchEnabled(false);
        busHolder.parallelHatch.setCurrentParallel(parallels);

        itemIn.setStackInSlot(0, COBBLE.copyWithCount(batches * parallels));
        fluidIn.setFluidInTank(0, new FluidStack(LCENT_IN, amount));

        // 1t to turn on, 1t per recipe run, 4t buffer for sanity
        // 16 parallels
        // check the results of all rolls together
        // repeat recipe MULTI_REPLICAS times
        int[] rolls = new int[MULTI_REPLICAS];
        for (int i = 1; i <= MULTI_REPLICAS; i++) {
            final int finalI = i; // lambda preserve you
            helper.runAfterDelay(21 * finalI, () -> {
                FluidStack results = fluidIn.getFluidInTank(0);
                int upperLimit = amount - (batches * parallels * 0);
                int lowerLimit = amount - (batches * parallels * 40);
                int completed = batches * parallels * finalI;
                helper.assertTrue(
                        TestUtils.isFluidStackEqual(new FluidStack(fluidOut.getFluidInTank(0),
                                ((int) Math.round(fluidOut.getTotalContentAmount()))),
                                new FluidStack(REDSTONE, completed)),
                        "Parallel LCent didn't complete correct number of recipes, completed [" +
                                ((int) Math.round(fluidOut.getTotalContentAmount())) + "] not [" + completed +
                                "].\n Current machine state: " + busHolder.controller.recipeLogic.getStatus() +
                                "\nFailed recipes follow:\n" + TestUtils.getFailures(busHolder.controller.recipeLogic));
                helper.assertTrue(TestUtils.isFluidWithinRange(results, lowerLimit, upperLimit),
                        "Parallel LCent didn't consume correct number of fluids, consumed " +
                                (amount - results.getAmount()) + "] not [" + lowerLimit + "-" + upperLimit + "]");

                rolls[finalI - 1] = amount - results.getAmount();

                // reset for a rerun
                itemIn.setStackInSlot(0, COBBLE.copyWithCount(batches * parallels));
                fluidIn.setFluidInTank(0, new FluidStack(LCENT_IN, 40 * batches * parallels));
            });
        }

        helper.runAfterDelay(1 + 21 * MULTI_REPLICAS, () -> {
            // check if each roll was a multiple of run count
            boolean sus = false;
            for (int i = 0; i < MULTI_REPLICAS; i++) {
                if (TestUtils.isStackSizeExactlyEvenMultiple(rolls[i], batches, parallels, 1)) {
                    sus = true;
                    GTCEu.LOGGER.warn("Parallel LCent ranged fluid input test iteration " + i + " consumed [" +
                            rolls[i] + "] fluids, a multiple of its Batch * Parallel count (" + (parallels) +
                            "). If this message only appears once, this is likely a false positive.");
                } else {
                    sus = false;
                    break;
                }
            }

            helper.assertFalse(sus, "Parallel LCent ranged fluid input test rolled exactly even to " +
                    "Batch * Parallel count on every iteration");
            helper.succeed();
        });
    }

    // test for multiblock machine with 16x Parallels with ranged fluid output
    @GameTest(template = "large_centrifuge_zpm_batch_parallel16",
              batch = "RangedFluidIngredients",
              timeoutTicks = 2000)
    public static void multiblockLCentRangedFluidOutput16Parallel(GameTestHelper helper) {
        BusHolderBatchParallel busHolder = getBussesAndFormLCENT(helper);

        final NotifiableFluidTank fluidIn = busHolder.inputHatch1.tank;
        final NotifiableFluidTank fluidOut = busHolder.outputHatch1.tank;

        int batches = 1; // unused on this test
        int parallels = 16;
        fluidIn.setFluidInTank(0, new FluidStack(LCENT_OUT, 16));

        busHolder.controller.setBatchEnabled(false);
        busHolder.parallelHatch.setCurrentParallel(parallels);

        // 1t to turn on, 1t per recipe run, 4t buffer for sanity
        // 16 parallels
        // check the results of all rolls together
        // repeat recipe MULTI_REPLICAS times
        int[] addedRolls = new int[MULTI_REPLICAS];
        for (int i = 1; i <= MULTI_REPLICAS; i++) {
            final int finalI = i; // lambda preserve you
            helper.runAfterDelay(21 * finalI, () -> {
                int runs = finalI * batches * parallels;
                helper.assertTrue(fluidIn.getFluidInTank(0).isEmpty(),
                        "Parallel LCent didn't complete correct number of recipes, completed [" +
                                fluidIn.getFluidInTank(0).getAmount() + "] not [" + runs +
                                "]\n Current machine state: " + busHolder.controller.recipeLogic.getStatus() +
                                "\nFailed recipes follow:\n" + TestUtils.getFailures(busHolder.controller.recipeLogic));
                int resultCount = (int) Math.round(fluidOut.getTotalContentAmount());
                int lowerLimit = runs * 0;
                int upperLimit = runs * 40;
                helper.assertTrue(TestUtils.isCountWithinRange(resultCount, lowerLimit, upperLimit),
                        "Parallel LCent didn't produce correct number of fluids, produced [" +
                                resultCount + "] not [" + lowerLimit + "-" + upperLimit + "]");

                addedRolls[finalI - 1] = resultCount;

                // reset for a rerun
                fluidIn.setFluidInTank(0, new FluidStack(LCENT_OUT, 16));
            });
        }

        helper.runAfterDelay(1 + 21 * MULTI_REPLICAS, () -> {
            // check if each roll was a multiple of run count
            boolean sus = false;
            int[] rolls = new int[MULTI_REPLICAS];

            rolls[0] = addedRolls[0];
            if (TestUtils.isStackSizeExactlyEvenMultiple(rolls[0], batches, parallels, 1)) {
                sus = true;
                GTCEu.LOGGER.warn("Parallel LCent ranged fluid output test iteration " + 1 + " produced [" +
                        rolls[0] + "] fluids, a multiple of its Batch * Parallel count (" + (batches * parallels) +
                        "). If this message only appears once, this is likely a false positive.");
            }
            for (int i = 1; i < MULTI_REPLICAS; i++) {
                rolls[i] = addedRolls[i] - addedRolls[i - 1];
                if (TestUtils.isStackSizeExactlyEvenMultiple(rolls[i], batches, parallels, 1)) {
                    sus = true;
                    GTCEu.LOGGER.warn("Parallel LCent ranged fluid output test iteration " + (i + 1) + " produced [" +
                            rolls[i] + "] fluids, a multiple of its Batch * Parallel count (" + (batches * parallels) +
                            "). If this message only appears once, this is likely a false positive.");
                } else {
                    sus = false;
                    break;
                }
            }

            helper.assertFalse(sus, "Parallel LCent ranged fluid output test rolled exactly even to " +
                    "Batch * Parallel count on every iteration");
            helper.succeed();
        });
    }

    // test for multiblock machine with 16x Parallels with ranged fluid input
    @GameTest(template = "large_centrifuge_zpm_batch_parallel16",
              batch = "RangedFluidIngredients",
              timeoutTicks = 2000)
    public static void multiblockLCentRangedFluidInputBatched(GameTestHelper helper) {
        BusHolderBatchParallel busHolder = getBussesAndFormLCENT(helper);

        NotifiableItemStackHandler itemIn = busHolder.inputBus1.getInventory();
        final NotifiableFluidTank fluidIn = busHolder.inputHatch1.tank;
        final NotifiableFluidTank fluidOut = busHolder.outputHatch1.tank;

        int batches = 16;
        int parallels = 1;
        final int amount = 40 * batches * parallels;
        busHolder.controller.setBatchEnabled(true);
        busHolder.parallelHatch.setCurrentParallel(parallels);

        itemIn.setStackInSlot(0, COBBLE.copyWithCount(batches * parallels));
        fluidIn.setFluidInTank(0, new FluidStack(LCENT_IN, amount));

        // 1t to turn on, 1t per recipe run, 4t buffer for sanity
        // 16 batches
        // check the results of all rolls together
        // repeat recipe MULTI_REPLICAS times
        int[] rolls = new int[MULTI_REPLICAS];
        for (int i = 1; i <= MULTI_REPLICAS; i++) {
            final int finalI = i; // lambda preserve you
            helper.runAfterDelay(21 * finalI, () -> {
                FluidStack results = fluidIn.getFluidInTank(0);
                int upperLimit = amount - (batches * parallels * 0);
                int lowerLimit = amount - (batches * parallels * 40);
                int completed = batches * parallels * finalI;
                helper.assertTrue(
                        TestUtils.isFluidStackEqual(new FluidStack(fluidOut.getFluidInTank(0),
                                ((int) Math.round(fluidOut.getTotalContentAmount()))),
                                new FluidStack(REDSTONE, completed)),
                        "Batched LCent didn't complete correct number of recipes, completed [" +
                                ((int) Math.round(fluidOut.getTotalContentAmount())) + "] not [" + completed +
                                "].\n Current machine state: " + busHolder.controller.recipeLogic.getStatus() +
                                "\nFailed recipes follow:\n" + TestUtils.getFailures(busHolder.controller.recipeLogic));
                helper.assertTrue(TestUtils.isFluidWithinRange(results, lowerLimit, upperLimit),
                        "Batched LCent didn't consume correct number of fluids, consumed " +
                                (amount - results.getAmount()) + "] not [" + lowerLimit + "-" + upperLimit + "]");

                rolls[finalI - 1] = amount - results.getAmount();

                // reset for a rerun
                itemIn.setStackInSlot(0, COBBLE.copyWithCount(batches * parallels));
                fluidIn.setFluidInTank(0, new FluidStack(LCENT_IN, amount));
            });
        }

        helper.runAfterDelay(1 + 21 * MULTI_REPLICAS, () -> {
            // check if each roll was a multiple of run count
            boolean sus = false;
            for (int i = 0; i < MULTI_REPLICAS; i++) {
                if (TestUtils.isStackSizeExactlyEvenMultiple(rolls[i], batches, parallels, 1)) {
                    sus = true;
                    GTCEu.LOGGER.warn("Batched LCent ranged fluid input test iteration " + i + " consumed [" +
                            rolls[i] + "] fluids, a multiple of its Batch * Parallel count (" + (batches * parallels) +
                            "). If this message only appears once, this is likely a false positive.");
                } else {
                    sus = false;
                    break;
                }
            }

            helper.assertFalse(sus, "Batched LCent ranged fluid input test rolled exactly even to " +
                    "Batch * Parallel count on every iteration");
            helper.succeed();
        });
    }

    // test for multiblock machine with 16x Parallels with ranged fluid output
    @GameTest(template = "large_centrifuge_zpm_batch_parallel16",
              batch = "RangedFluidIngredients",
              timeoutTicks = 2000)
    public static void multiblockLCentRangedFluidOutputBatched(GameTestHelper helper) {
        BusHolderBatchParallel busHolder = getBussesAndFormLCENT(helper);

        final NotifiableFluidTank fluidIn = busHolder.inputHatch1.tank;
        final NotifiableFluidTank fluidOut = busHolder.outputHatch1.tank;

        int batches = 16;
        int parallels = 1; // unused on this test
        fluidIn.setFluidInTank(0, new FluidStack(LCENT_OUT, 16));

        busHolder.controller.setBatchEnabled(true);
        busHolder.parallelHatch.setCurrentParallel(parallels);

        // 1t to turn on, 1t per recipe run, 4t buffer for sanity
        // 16 parallels
        // check the results of all rolls together
        // repeat recipe MULTI_REPLICAS times
        int[] addedRolls = new int[MULTI_REPLICAS];
        for (int i = 1; i <= MULTI_REPLICAS; i++) {
            final int finalI = i; // lambda preserve you
            helper.runAfterDelay(21 * finalI, () -> {
                int runs = finalI * batches * parallels;
                helper.assertTrue(fluidIn.getFluidInTank(0).isEmpty(),
                        "Batched LCent didn't complete correct number of recipes, completed [" +
                                fluidIn.getFluidInTank(0).getAmount() + "] not [" + runs +
                                "]\n Current machine state: " + busHolder.controller.recipeLogic.getStatus() +
                                "\nFailed recipes follow:\n" + TestUtils.getFailures(busHolder.controller.recipeLogic));
                int resultCount = (int) Math.round(fluidOut.getTotalContentAmount());
                int lowerLimit = runs * 0;
                int upperLimit = runs * 40;
                helper.assertTrue(TestUtils.isCountWithinRange(resultCount, lowerLimit, upperLimit),
                        "Batched LCent didn't produce correct number of fluids, produced [" +
                                resultCount + "] not [" + lowerLimit + "-" + upperLimit + "]");

                addedRolls[finalI - 1] = resultCount;

                // reset for a rerun
                fluidIn.setFluidInTank(0, new FluidStack(LCENT_OUT, 16));
            });
        }

        helper.runAfterDelay(1 + 21 * MULTI_REPLICAS, () -> {
            // check if each roll was a multiple of run count
            boolean sus = false;
            int[] rolls = new int[MULTI_REPLICAS];

            rolls[0] = addedRolls[0];
            if (TestUtils.isStackSizeExactlyEvenMultiple(rolls[0], batches, parallels, 1)) {
                sus = true;
                GTCEu.LOGGER.warn("Batched LCent ranged fluid output test iteration " + 1 + " produced [" +
                        rolls[0] + "] fluids, a multiple of its Batch * Parallel count (" + batches +
                        "). If this message only appears once, this is likely a false positive.");
            }
            for (int i = 1; i < MULTI_REPLICAS; i++) {
                rolls[i] = addedRolls[i] - addedRolls[i - 1];
                if (TestUtils.isStackSizeExactlyEvenMultiple(rolls[i], batches, parallels, 1)) {
                    sus = true;
                    GTCEu.LOGGER.warn("Batched LCent ranged fluid output test iteration " + (i + 1) + " produced [" +
                            rolls[i] + "] fluids, a multiple of its Batch * Parallel count (" + batches +
                            "). If this message only appears once, this is likely a false positive.");
                } else {
                    sus = false;
                    break;
                }
            }

            helper.assertFalse(sus, "Batched LCent ranged fluid output test rolled exactly even to " +
                    "Batch * Parallel count on every iteration");
            helper.succeed();
        });
    }

    // test for multiblock machine with 16x Parallels with ranged fluid input
    @GameTest(template = "large_centrifuge_zpm_batch_parallel16",
              batch = "RangedFluidIngredients",
              timeoutTicks = 2000)
    public static void multiblockLCentRangedFluidInput16ParallelBatched(GameTestHelper helper) {
        BusHolderBatchParallel busHolder = getBussesAndFormLCENT(helper);

        NotifiableItemStackHandler itemIn = busHolder.inputBus1.getInventory();
        final NotifiableFluidTank fluidIn = busHolder.inputHatch1.tank;
        final NotifiableFluidTank fluidOut = busHolder.outputHatch1.tank;

        int batches = 16;
        int parallels = 16;
        final int amount = batches * parallels * 40;
        busHolder.controller.setBatchEnabled(true);
        busHolder.parallelHatch.setCurrentParallel(parallels);

        int stacks = batches * parallels / 64;

        for (int j = 0; j < stacks; j++) {
            itemIn.setStackInSlot(j, COBBLE.copyWithCount((batches * parallels / stacks)));
        }
        fluidIn.setFluidInTank(0, new FluidStack(LCENT_IN, amount));

        // 1t to turn on, 64t per recipe run, 10t buffer for sanity
        // check the results of all rolls together
        // repeat recipe MULTI_REPLICAS times
        int[] rolls = new int[MULTI_REPLICAS];
        for (int i = 1; i <= MULTI_REPLICAS; i++) {
            final int finalI = i; // lambda preserve you
            helper.runAfterDelay(75 * finalI, () -> {
                FluidStack results = fluidIn.getFluidInTank(0);
                int completed = batches * parallels * finalI;
                helper.assertTrue(
                        TestUtils.isFluidStackEqual(
                                new FluidStack(fluidOut.getFluidInTank(0), fluidOut.getFluidInTank(0).getAmount()),
                                new FluidStack(REDSTONE, completed)),
                        "Batched Parallel LCent didn't complete correct number of recipes, completed [" +
                                (fluidOut.getFluidInTank(0).getAmount()) + "] not [" + completed +
                                "]\n Current machine state: " + busHolder.controller.recipeLogic.getStatus() +
                                "\nFailed recipes follow:\n" + TestUtils.getFailures(busHolder.controller.recipeLogic));
                int upperLimit = amount - (batches * parallels * 0);
                int lowerLimit = amount - (batches * parallels * 40);
                helper.assertTrue(TestUtils.isFluidWithinRange(results, lowerLimit, upperLimit),
                        "Batched Parallel LCent didn't consume correct number of fluids, consumed " +
                                (amount - results.getAmount()) + "] not [" + lowerLimit + "-" + upperLimit + "]");

                rolls[finalI - 1] = 64 - results.getAmount();

                // reset for a rerun
                for (int l = 0; l < stacks; l++) {
                    itemIn.setStackInSlot(l,
                            COBBLE.copyWithCount((batches * parallels / stacks)));
                }
                fluidIn.setFluidInTank(0, new FluidStack(LCENT_IN, 40 * batches * parallels));
            });
        }

        helper.runAfterDelay(1 + 75 * MULTI_REPLICAS, () -> {
            // check if each roll was a multiple of run count
            boolean sus = false;
            for (int i = 0; i < MULTI_REPLICAS; i++) {
                if (TestUtils.isStackSizeExactlyEvenMultiple(rolls[i], batches, parallels, 1)) {
                    sus = true;
                    GTCEu.LOGGER.warn("Batched Parallel LCent ranged fluid input test iteration " + i + " consumed [" +
                            rolls[i] + "] fluids, a multiple of its Batch * Parallel count (" + (batches * parallels) +
                            "). If this message only appears once, this is likely a false positive.");
                } else {
                    sus = false;
                    break;
                }
            }

            helper.assertFalse(sus, "Batched Parallel LCent ranged fluid input test rolled exactly even to" +
                    " Batch * Parallel count on every iteration");
            helper.succeed();
        });
    }

    // test for multiblock machine with 16x Parallels with ranged fluid output
    @GameTest(template = "large_centrifuge_zpm_batch_parallel16",
              batch = "RangedFluidIngredients",
              timeoutTicks = 2000)
    public static void multiblockLCentRangedFluidOutput16ParallelBatched(GameTestHelper helper) {
        BusHolderBatchParallel busHolder = getBussesAndFormLCENT(helper);

        final NotifiableFluidTank fluidIn = busHolder.inputHatch1.tank;
        final NotifiableFluidTank fluidOut = busHolder.outputHatch1.tank;

        int batches = 16;
        int parallels = 16;
        busHolder.controller.setBatchEnabled(true);
        busHolder.parallelHatch.setCurrentParallel(parallels);

        fluidIn.setFluidInTank(0, new FluidStack(LCENT_OUT, batches * parallels));

        // 1t to turn on, 64t per recipe run, 10t buffer for sanity
        // check the results of all rolls together
        // repeat recipe MULTI_REPLICAS times
        int[] addedRolls = new int[MULTI_REPLICAS];
        for (int i = 1; i <= MULTI_REPLICAS; i++) {
            final int finalI = i; // lambda preserve you
            helper.runAfterDelay(75 * finalI, () -> {
                int runs = finalI * batches * parallels;
                helper.assertTrue(fluidIn.isEmpty(),
                        "Batched Parallel LCent didn't complete correct number of recipes, completed [" +
                                (runs - fluidIn.getFluidInTank(0).getAmount()) + "] not [" + runs +
                                "]\n Current machine state: " + busHolder.controller.recipeLogic.getStatus() +
                                "\nFailed recipes follow:\n" + TestUtils.getFailures(busHolder.controller.recipeLogic));
                int resultCount = fluidOut.getFluidInTank(0).getAmount();
                int lowerLimit = runs * 0;
                int upperLimit = runs * 40;
                helper.assertTrue(TestUtils.isCountWithinRange(resultCount, lowerLimit, upperLimit),
                        "Batched Parallel LCent didn't produce correct number of fluids, produced [" +
                                resultCount + "] not [" + lowerLimit + "-" + upperLimit + "]");

                addedRolls[finalI - 1] = resultCount;

                // reset for a rerun
                fluidIn.setFluidInTank(0, new FluidStack(LCENT_OUT, batches * parallels));
            });
        }

        helper.runAfterDelay(1 + 75 * MULTI_REPLICAS, () -> {
            // check if each roll was a multiple of run count
            boolean sus = false;
            int[] rolls = new int[MULTI_REPLICAS];

            rolls[0] = addedRolls[0];
            if (TestUtils.isStackSizeExactlyEvenMultiple(rolls[0], batches, parallels, 1)) {
                sus = true;
                GTCEu.LOGGER.warn("Batched Parallel LCent ranged fluid output test iteration " + 1 + " produced [" +
                        rolls[0] + "] fluids, a multiple of its Batch * Parallel count (" + (batches * parallels) +
                        "). If this message only appears once, this is likely a false positive.");
            }
            for (int i = 1; i < MULTI_REPLICAS; i++) {
                rolls[i] = addedRolls[i] - addedRolls[i - 1];
                if (TestUtils.isStackSizeExactlyEvenMultiple(rolls[i], batches, parallels, 1)) {
                    sus = true;
                    GTCEu.LOGGER.warn("Batched Parallel LCent ranged fluid output test iteration " + (i + 1) +
                            " produced [" + rolls[i] + "] fluids, a multiple of its Batch * Parallel count (" +
                            (batches * parallels) + "). If this message only appears once, this is likely" +
                            " a false positive.");
                } else {
                    sus = false;
                    break;
                }
            }

            helper.assertFalse(sus, "Batched Parallel LCent ranged fluid output test rolled exactly even to" +
                    " Batch * Parallel count on every iteration");
            helper.succeed();
        });
    }

    // test for multiblock machine with 16x Parallels with ranged fluid output preroll
    @GameTest(template = "large_centrifuge_zpm_batch_parallel16",
              batch = "RangedFluidIngredients",
              timeoutTicks = 2000)
    public static void multiblockLCentRangedFluidOutputPreroll16ParallelBatched(GameTestHelper helper) {
        BusHolderBatchParallel busHolder = getBussesAndFormLCENT(helper);

        final NotifiableFluidTank fluidIn = busHolder.inputHatch1.tank;

        int batches = 16;
        int parallels = 16;
        busHolder.controller.setBatchEnabled(true);
        busHolder.parallelHatch.setCurrentParallel(parallels);

        fluidIn.setFluidInTank(0, new FluidStack(LCENT_OUT, batches * parallels));

        // 1t to turn on, 64t per recipe run, 10t buffer for sanity
        for (int i = 0; i < MULTI_REPLICAS; i++) {
            final int finalI = i; // lambda preserve you
            helper.runAfterDelay(75 * finalI + 20, () -> {
                helper.assertFalse(busHolder.controller.recipeLogic.getLastRecipe() == null,
                        "Multiblock LCent fluid Preroll was not running a recipe when preroll was checked!");
                var outputPrerolls = busHolder.controller.recipeLogic.getLastRecipe().outputs
                        .get(FluidRecipeCapability.CAP);
                helper.assertFalse(outputPrerolls.size() == 0,
                        "Multiblock LCent fluid Preroll's recipe output contained no fluids!");
                helper.assertFalse(outputPrerolls.get(0).content() instanceof IRangedIngredient,
                        "Multiblock LCent fluid Preroll's recipe failed to preroll and replace its " +
                                "ranged ingredient!");
                // reset for a rerun
                fluidIn.setFluidInTank(0, new FluidStack(LCENT_OUT, batches * parallels));
            });
        }
        TestUtils.succeedAfterTest(helper);
    }

    // Test for singleblock machine with per-tick ranged Fluid input
    @GameTest(template = "singleblock_charged_cr", batch = "RangedFluidIngredients")
    public static void singleblockRangedTickFluidInput(GameTestHelper helper) {
        SimpleTieredMachine machine = (SimpleTieredMachine) getMetaMachine(
                helper.getBlockEntity(new BlockPos(0, 1, 0)));

        machine.setRecipeType(CR_RECIPE_TYPE);
        NotifiableItemStackHandler itemIn = (NotifiableItemStackHandler) machine
                .getCapabilitiesFlat(IO.IN, ItemRecipeCapability.CAP).get(0);
        NotifiableItemStackHandler itemOut = (NotifiableItemStackHandler) machine
                .getCapabilitiesFlat(IO.OUT, ItemRecipeCapability.CAP).get(0);
        NotifiableFluidTank fluidIn = (NotifiableFluidTank) machine
                .getCapabilitiesFlat(IO.IN, FluidRecipeCapability.CAP).get(0);
        NotifiableFluidTank fluidOut = (NotifiableFluidTank) machine
                .getCapabilitiesFlat(IO.OUT, FluidRecipeCapability.CAP).get(0);

        fluidIn.setFluidInTank(0, new FluidStack(CR_TICK_IN, 64));
        itemIn.setStackInSlot(1, COBBLE.copyWithCount(1));
        // 1t to turn on, 7t recipe run
        // get the result of each roll independently
        int[] addedRolls = new int[7];
        for (int i = 0; i < 7; i++) {
            final int finalI = i; // lambda preserve you
            helper.runAfterDelay(i + 2, () -> {
                addedRolls[finalI] = fluidIn.getFluidInTank(0).getAmount();
            });
        }
        // check the results of all rolls together
        helper.runAfterDelay(7 + 5, () -> {
            FluidStack results = fluidIn.getFluidInTank(0);
            int upperLimit = 64 - (7 * 0);
            int lowerLimit = 64 - (7 * 9);
            helper.assertTrue(TestUtils.isItemStackEqual(itemOut.getStackInSlot(0), STONE.copyWithCount(1)),
                    "Singleblock per-tick fluid CR didn't complete correct number of recipes, completed [" +
                            itemOut.getStackInSlot(0).getCount() + "] not [" + 1 + "]");
            helper.assertFalse((results.getAmount() == lowerLimit),
                    "Singleblock per-tick fluid CR rolled max value on every roll");
            helper.assertFalse((results.getAmount() == upperLimit),
                    "Singleblock per-tick fluid CR rolled min value on every roll");

            // check if all the rolls were equal, but not min/max
            int[] rolls = new int[7];
            rolls[0] = 64 - addedRolls[0];
            boolean allEqual = false;
            for (int i = 1; i < 7; i++) {
                rolls[i] = addedRolls[i - 1] - addedRolls[i];
                if (rolls[i] == rolls[i - 1]) {
                    allEqual = true;
                } else {
                    allEqual = false;
                    break;
                }
            }
            helper.assertFalse(allEqual,
                    "Singleblock per-tick fluid CR rolled the same value on every input roll (rolled " + rolls[0] +
                            ")");
            helper.succeed();
        });
    }

    // Test for singleblock machine with per-tick ranged Fluid output
    @GameTest(template = "singleblock_charged_cr", batch = "RangedFluidIngredients")
    public static void singleblockRangedTickFluidOutput(GameTestHelper helper) {
        SimpleTieredMachine machine = (SimpleTieredMachine) getMetaMachine(
                helper.getBlockEntity(new BlockPos(0, 1, 0)));

        machine.setRecipeType(CR_RECIPE_TYPE);
        NotifiableItemStackHandler itemIn = (NotifiableItemStackHandler) machine
                .getCapabilitiesFlat(IO.IN, ItemRecipeCapability.CAP).get(0);
        NotifiableItemStackHandler itemOut = (NotifiableItemStackHandler) machine
                .getCapabilitiesFlat(IO.OUT, ItemRecipeCapability.CAP).get(0);
        NotifiableFluidTank fluidIn = (NotifiableFluidTank) machine
                .getCapabilitiesFlat(IO.IN, FluidRecipeCapability.CAP).get(0);
        NotifiableFluidTank fluidOut = (NotifiableFluidTank) machine
                .getCapabilitiesFlat(IO.OUT, FluidRecipeCapability.CAP).get(0);

        itemIn.setStackInSlot(0, STONE.copyWithCount(1));
        // 1t to turn on, 2t per recipe run
        // get the result of each roll independently
        int[] addedRolls = new int[7];
        for (int i = 0; i < 7; i++) {
            final int finalI = i; // lambda preserve you
            helper.runAfterDelay(i + 2, () -> {
                machine.getRecipeTypes();
                addedRolls[finalI] = fluidOut.getFluidInTank(0).getAmount();
            });
        }
        // check the results of all rolls together
        helper.runAfterDelay(7 + 5, () -> {
            FluidStack results = fluidOut.getFluidInTank(0);
            helper.assertFalse((results.getAmount() == 7 * 9),
                    "Singleblock per-tick CR rolled max value on every roll");
            helper.assertFalse((results.getAmount() == 7 * 0),
                    "Singleblock per-tick CR rolled min value on every roll");

            // check if all the rolls were equal, but not min/max
            int[] rolls = new int[7];
            rolls[0] = addedRolls[0];
            boolean allEqual = false;
            for (int i = 1; i < 7; i++) {
                rolls[i] = addedRolls[i] - addedRolls[i - 1];
                if (rolls[i] == rolls[i - 1]) {
                    allEqual = true;
                } else {
                    allEqual = false;
                    break;
                }
            }
            helper.assertFalse(allEqual,
                    "Singleblock per-tick CR rolled the same value on every input roll (rolled " + rolls[0] + ")");
            helper.succeed();
        });
    }

    // test for multiblock machine with Batching and 16x Parallels with per-tick ranged Fluid input
    @GameTest(template = "large_centrifuge_zpm_batch_parallel16",
              batch = "RangedFluidIngredients")
    public static void multiblockLCentRangedTickFluidInput16ParallelBatched(GameTestHelper helper) {
        BusHolderBatchParallel busHolder = getBussesAndFormLCENT(helper);

        NotifiableItemStackHandler itemIn = busHolder.inputBus1.getInventory();
        NotifiableItemStackHandler itemOut = busHolder.outputBus1.getInventory();
        NotifiableFluidTank fluidIn = busHolder.inputHatch1.tank;
        NotifiableFluidTank fluidOut = busHolder.outputHatch1.tank;

        int batches = 16;
        int parallels = 16;
        busHolder.controller.setBatchEnabled(true);
        busHolder.parallelHatch.setCurrentParallel(parallels);

        int j;
        int stacks = batches * parallels / 64;
        final int amount = batches * parallels * 64 * 9;

        for (j = 0; j < stacks; j++) {
            itemIn.setStackInSlot(j, COBBLE.copyWithCount((batches * parallels / stacks)));
        }
        fluidIn.setFluidInTank(0, new FluidStack(CR_TICK_IN, amount));

        // 1t to turn on, 64t recipe run
        // 16 parallels 16 batches
        int[] rolls = new int[64];
        for (int i = 1; i <= 64; i++) {
            final int finalI = i; // lambda preserve you
            helper.runAfterDelay(finalI, () -> {
                rolls[finalI - 1] = (int) fluidIn.getTotalContentAmount();
            });
        }

        helper.runAfterDelay(75, () -> {
            // check if each roll was a multiple of run count
            boolean sus = true;
            for (int i = 0; i < rolls.length; i++) {
                if (TestUtils.isStackSizeExactlyEvenMultiple(rolls[i], batches, parallels, 1)) {
                    GTCEu.LOGGER.warn("Batched Parallel LCent ranged tick fluid input test iteration " + i +
                            " consumed [" +
                            rolls[i] + "] items, a multiple of its Batch * Parallel count (" + (batches * parallels) +
                            "). If this message only appears once, this is likely a false positive.");
                } else if (sus) {
                    sus = false;
                    break;
                }
            }

            helper.assertFalse(sus, "Batched Parallel LCent ranged tick fluid input test rolled exactly even to" +
                    " Batch * Parallel count on every iteration");
            helper.succeed();
        });
    }

    // test for multiblock machine with Batching 16x Parallels with per-tick ranged Fluid output
    @GameTest(template = "large_centrifuge_zpm_batch_parallel16",
              batch = "RangedFluidIngredients")
    public static void multiblockLCentRangedTickFluidOutput16ParallelBatched(GameTestHelper helper) {
        BusHolderBatchParallel busHolder = getBussesAndFormLCENT(helper);

        NotifiableItemStackHandler itemIn = busHolder.inputBus1.getInventory();
        NotifiableItemStackHandler itemOut = busHolder.outputBus1.getInventory();
        NotifiableFluidTank fluidIn = busHolder.inputHatch1.tank;
        NotifiableFluidTank fluidOut = busHolder.outputHatch1.tank;

        int batches = 16;
        int parallels = 16;
        busHolder.controller.setBatchEnabled(true);
        busHolder.parallelHatch.setCurrentParallel(parallels);
        int amount = batches * parallels;

        fluidIn.setFluidInTank(0, new FluidStack(CR_TICK_OUT, amount));

        // 1t to turn on, 64t per recipe run, 10t buffer for sanity
        // 16 parallels
        // check the results of all rolls together
        // repeat recipe MULTI_REPLICAS times
        int[] rolls = new int[64];
        for (int i = 1; i <= 64; i++) {
            final int finalI = i; // lambda preserve you
            helper.runAfterDelay(finalI, () -> {
                rolls[finalI - 1] = (int) fluidOut.getTotalContentAmount();
            });
        }

        helper.runAfterDelay(75, () -> {
            // check if each roll was a multiple of run count
            boolean sus = true;
            for (int i = 0; i < rolls.length; i++) {
                if (TestUtils.isStackSizeExactlyEvenMultiple(rolls[i], batches, parallels, 1)) {
                    GTCEu.LOGGER.warn("Batched Parallel LCent ranged tick fluid output test iteration " + i +
                            " produced [" +
                            rolls[i] + "] items, a multiple of its Batch * Parallel count (" + (batches * parallels) +
                            "). If this message only appears once, this is likely a false positive.");
                } else if (sus) {
                    sus = false;
                    break;
                }
            }

            helper.assertFalse(sus, "Batched Parallel LCent ranged tick fluid output test rolled exactly even to" +
                    " Batch * Parallel count on every iteration");
            helper.succeed();
        });
    }
}
