001/*
002 * Copyright © 2025 CUI-OpenSource-Software (info@cuioss.de)
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package de.cuioss.test.generator.junit.parameterized;
017
018import de.cuioss.test.generator.Generators;
019import de.cuioss.test.generator.TypedGenerator;
020import org.junit.jupiter.api.extension.ExtensionContext;
021import org.junit.jupiter.params.provider.Arguments;
022import org.junit.jupiter.params.support.AnnotationConsumer;
023import org.junit.platform.commons.JUnitException;
024
025import java.util.ArrayList;
026import java.util.List;
027import java.util.stream.Stream;
028
029/**
030 * Implementation of {@link org.junit.jupiter.params.provider.ArgumentsProvider} that provides arguments from multiple
031 * {@link TypedGenerator} instances for parameterized tests annotated with
032 * {@link CompositeTypeGeneratorSource}.
033 * 
034 * <p>
035 * This provider creates multiple {@link TypedGenerator} instances from the specified
036 * classes and/or methods, and generates combinations of values to be used as test arguments.
037 * </p>
038 * 
039 * <p>
040 * Seed management is integrated with the existing
041 * {@link de.cuioss.test.generator.junit.GeneratorControllerExtension} to ensure
042 * consistent and reproducible test data generation.
043 * </p>
044 * 
045 * @author Oliver Wolff
046 * @since 2.0
047 * @see CompositeTypeGeneratorSource
048 * @see TypedGenerator
049 */
050public class CompositeTypeGeneratorArgumentsProvider extends AbstractTypedGeneratorArgumentsProvider
051        implements AnnotationConsumer<CompositeTypeGeneratorSource> {
052
053    private Class<? extends TypedGenerator<?>>[] generatorClasses;
054    private String[] generatorMethods;
055    private GeneratorType[] generators;
056    private int count;
057    private boolean cartesianProduct;
058
059    @Override
060    public void accept(CompositeTypeGeneratorSource annotation) {
061        generatorClasses = annotation.generatorClasses();
062        generatorMethods = annotation.generatorMethods();
063        generators = annotation.generators();
064        count = Math.max(1, annotation.count());
065        cartesianProduct = annotation.cartesianProduct();
066    }
067
068    @Override
069    protected Stream<? extends Arguments> provideArgumentsForGenerators(ExtensionContext context) {
070        if (generatorClasses.length == 0 && generatorMethods.length == 0 && generators.length == 0) {
071            throw new JUnitException("At least one generator class, method, or type must be specified");
072        }
073
074        // Create generator instances
075        List<TypedGenerator<?>> generatorInstances = new ArrayList<>();
076
077        // Add generators from classes
078        for (Class<? extends TypedGenerator<?>> generatorClass : generatorClasses) {
079            generatorInstances.add(createGeneratorInstance(generatorClass));
080        }
081
082        // Add generators from methods
083        for (String methodName : generatorMethods) {
084            generatorInstances.add(GeneratorMethodResolver.getGenerator(methodName, context));
085        }
086
087        // Add generators from GeneratorType enum values
088        for (GeneratorType generatorType : generators) {
089            generatorInstances.add(createGeneratorFromType(generatorType));
090        }
091
092        // Generate values from each generator
093        List<List<Object>> generatedValues = new ArrayList<>();
094        for (TypedGenerator<?> generator : generatorInstances) {
095            List<Object> values = new ArrayList<>();
096            for (int i = 0; i < count; i++) {
097                values.add(generator.next());
098            }
099            generatedValues.add(values);
100        }
101
102        // Create combinations of values
103        return createArgumentsCombinations(generatedValues);
104    }
105
106    @Override
107    protected long getSeed() {
108        return -1L;
109    }
110
111    @Override
112    protected int getCount() {
113        return count;
114    }
115
116    /**
117     * Creates combinations of values from the generated values.
118     * 
119     * @param generatedValues lists of values from each generator
120     * @return a stream of Arguments with combinations of values
121     */
122    private Stream<Arguments> createArgumentsCombinations(List<List<Object>> generatedValues) {
123        if (generatedValues.isEmpty()) {
124            return Stream.empty();
125        }
126
127        if (cartesianProduct) {
128            return createCartesianProduct(generatedValues);
129        } else {
130            return createOneToOnePairs(generatedValues);
131        }
132    }
133
134    /**
135     * Creates a cartesian product of all generated values.
136     * 
137     * @param generatedValues lists of values from each generator
138     * @return a stream of Arguments with all possible combinations
139     */
140    private Stream<Arguments> createCartesianProduct(List<List<Object>> generatedValues) {
141        // Start with a single empty list
142        List<List<Object>> combinations = new ArrayList<>();
143        combinations.add(new ArrayList<>());
144
145        // For each list of generated values
146        for (List<Object> values : generatedValues) {
147            List<List<Object>> newCombinations = new ArrayList<>();
148
149            // For each existing combination
150            for (List<Object> combination : combinations) {
151                // For each value in the current list
152                for (Object value : values) {
153                    // Create a new combination by adding the value to the existing combination
154                    List<Object> newCombination = new ArrayList<>(combination);
155                    newCombination.add(value);
156                    newCombinations.add(newCombination);
157                }
158            }
159
160            combinations = newCombinations;
161        }
162
163        // Convert combinations to Arguments
164        return combinations.stream()
165                .map(combination -> Arguments.of(combination.toArray()));
166    }
167
168    /**
169     * Creates one-to-one pairs of generated values.
170     * Requires all generators to produce the same number of values.
171     * 
172     * @param generatedValues lists of values from each generator
173     * @return a stream of Arguments with one-to-one pairs
174     * @throws JUnitException if the lists have different sizes
175     */
176    private Stream<Arguments> createOneToOnePairs(List<List<Object>> generatedValues) {
177        // Check that all lists have the same size
178        int size = generatedValues.getFirst().size();
179        for (List<Object> values : generatedValues) {
180            if (values.size() != size) {
181                throw new JUnitException(
182                        "When cartesianProduct is false, all generators must produce the same number of values");
183            }
184        }
185
186        // Create one-to-one pairs
187        List<Arguments> arguments = new ArrayList<>();
188        for (int i = 0; i < size; i++) {
189            Object[] args = new Object[generatedValues.size()];
190            for (int j = 0; j < generatedValues.size(); j++) {
191                args[j] = generatedValues.get(j).get(i);
192            }
193            arguments.add(Arguments.of(args));
194        }
195
196        return arguments.stream();
197    }
198
199    /**
200     * Creates a TypedGenerator from a GeneratorType enum value.
201     * 
202     * @param generatorType the generator type
203     * @return a TypedGenerator instance
204     * @throws JUnitException if the generator cannot be created
205     */
206    @SuppressWarnings("java:S1452") // This wildcard is because of the TypedGenerator interface. Ok for testing
207    private TypedGenerator<?> createGeneratorFromType(GeneratorType generatorType) {
208        try {
209            // Use reflection to invoke the method on Generators class
210            var methodName = generatorType.getMethodName();
211            var method = Generators.class.getMethod(methodName);
212            return (TypedGenerator<?>) method.invoke(null);
213        } catch (Exception e) {
214            throw new JUnitException("Failed to create generator from type: " + generatorType, e);
215        }
216    }
217}