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.lang.reflect.Constructor;
026import java.lang.reflect.InvocationTargetException;
027import java.lang.reflect.Method;
028import java.util.stream.Stream;
029
030import static java.util.Objects.requireNonNull;
031
032/**
033 * Implementation of {@link org.junit.jupiter.params.provider.ArgumentsProvider} that provides arguments from a
034 * {@link TypedGenerator} created by the {@link Generators} utility class for parameterized tests
035 * annotated with {@link GeneratorsSource}.
036 *
037 * <p>
038 * This provider invokes the specified generator method from the {@link Generators} class to obtain a {@link TypedGenerator}
039 * instance and generates the requested number of values to be used as test arguments.
040 * </p>
041 *
042 * <p>
043 * Seed management is integrated with the existing
044 * {@link de.cuioss.test.generator.junit.GeneratorControllerExtension} to ensure
045 * consistent and reproducible test data generation.
046 * </p>
047 *
048 * @author Oliver Wolff
049 * @see GeneratorsSource
050 * @see TypedGenerator
051 * @see Generators
052 * @since 2.0
053 */
054public class GeneratorsSourceArgumentsProvider extends AbstractTypedGeneratorArgumentsProvider
055        implements AnnotationConsumer<GeneratorsSource> {
056
057    private GeneratorType generatorType;
058    private int minSize;
059    private int maxSize;
060    private String low;
061    private String high;
062    private int count;
063
064    @Override
065    public void accept(GeneratorsSource annotation) {
066        generatorType = annotation.generator();
067
068        minSize = annotation.minSize();
069        maxSize = annotation.maxSize();
070        low = annotation.low();
071        high = annotation.high();
072        count = Math.max(1, annotation.count());
073    }
074
075    @Override
076    protected Stream<? extends Arguments> provideArgumentsForGenerators(ExtensionContext context) {
077        // Create generator instance using factory
078        TypedGenerator<?> generator;
079        try {
080            generator = createGeneratorFromFactory();
081        } catch (InvocationTargetException | NoSuchMethodException | InstantiationException |
082                IllegalAccessException e) {
083            throw new IllegalStateException(e);
084        }
085
086        // Generate values
087        return generateArguments(generator).stream();
088    }
089
090    @Override
091    protected long getSeed() {
092        return -1L;
093    }
094
095    @Override
096    protected int getCount() {
097        return count;
098    }
099
100    /**
101     * Creates a TypedGenerator instance based on the generator type.
102     *
103     * @return a TypedGenerator instance
104     * @throws JUnitException if the generator cannot be created
105     */
106    private TypedGenerator<?> createGeneratorFromFactory() throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException {
107        requireNonNull(generatorType, "Generator type must not be null");
108
109        // Check if this is a domain-specific generator (factory class is the generator class itself)
110        if (isDomainSpecificGenerator()) {
111            return createDomainSpecificGenerator();
112        }
113
114        // Otherwise, it's a standard generator from the Generators class
115        String methodName = generatorType.getMethodName();
116        requireNonNull(methodName, "Generator method name must not be null");
117
118        return switch (generatorType.getParameterType()) {
119            case NEEDS_BOUNDS -> createStringGenerator();
120            case NEEDS_RANGE -> createNumberGenerator();
121            case PARAMETERLESS -> createParameterlessGenerator();
122        };
123    }
124
125    /**
126     * Checks if the generator type is a domain-specific generator.
127     *
128     * @return true if it's a domain-specific generator
129     */
130    private boolean isDomainSpecificGenerator() {
131        return generatorType.getMethodName() == null &&
132                generatorType.getFactoryClass() != null &&
133                TypedGenerator.class.isAssignableFrom(generatorType.getFactoryClass());
134    }
135
136    /**
137     * Creates a domain-specific generator by instantiating the generator class.
138     *
139     * @return a TypedGenerator instance
140     */
141    private TypedGenerator<?> createDomainSpecificGenerator() throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
142        Class<?> generatorClass = generatorType.getFactoryClass();
143        Constructor<?> constructor = generatorClass.getDeclaredConstructor();
144        return (TypedGenerator<?>) constructor.newInstance();
145    }
146
147    /**
148     * Creates a string-based generator using the minSize and maxSize parameters.
149     *
150     * @return a TypedGenerator for strings
151     */
152    private TypedGenerator<?> createStringGenerator() throws InvocationTargetException, IllegalAccessException {
153        Method method = findMethodWithParameters(generatorType.getMethodName(), int.class, int.class);
154        return (TypedGenerator<?>) method.invoke(null, minSize, maxSize);
155    }
156
157    /**
158     * Creates a number-based generator using the low and high parameters.
159     *
160     * @return a TypedGenerator for numbers
161     */
162    private TypedGenerator<?> createNumberGenerator() throws InvocationTargetException, IllegalAccessException {
163        // Determine the parameter types based on the generator return type
164        Class<?>[] paramTypes;
165        Object[] params;
166
167        Class<?> returnType = generatorType.getReturnType();
168
169        if (Integer.class.equals(returnType)) {
170            paramTypes = new Class<?>[]{int.class, int.class};
171            params = new Object[]{Integer.parseInt(low), Integer.parseInt(high)};
172        } else if (Long.class.equals(returnType)) {
173            paramTypes = new Class<?>[]{long.class, long.class};
174            params = new Object[]{Long.parseLong(low), Long.parseLong(high)};
175        } else if (Double.class.equals(returnType)) {
176            paramTypes = new Class<?>[]{double.class, double.class};
177            params = new Object[]{Double.parseDouble(low), Double.parseDouble(high)};
178        } else if (Float.class.equals(returnType)) {
179            paramTypes = new Class<?>[]{float.class, float.class};
180            params = new Object[]{Float.parseFloat(low), Float.parseFloat(high)};
181        } else if (Short.class.equals(returnType)) {
182            paramTypes = new Class<?>[]{short.class, short.class};
183            params = new Object[]{Short.parseShort(low), Short.parseShort(high)};
184        } else if (Byte.class.equals(returnType)) {
185            paramTypes = new Class<?>[]{byte.class, byte.class};
186            params = new Object[]{Byte.parseByte(low), Byte.parseByte(high)};
187        } else {
188            throw new UnsupportedOperationException(
189                    "Number generator for type '" + returnType.getSimpleName() + "' is not supported.");
190        }
191
192        Method method = findMethodWithParameters(generatorType.getMethodName(), paramTypes);
193        return (TypedGenerator<?>) method.invoke(null, params);
194    }
195
196    /**
197     * Creates a generator that doesn't require parameters.
198     *
199     * @return a TypedGenerator
200     */
201    private TypedGenerator<?> createParameterlessGenerator() throws InvocationTargetException, IllegalAccessException {
202        Method method = findMethodWithParameters(generatorType.getMethodName());
203        return (TypedGenerator<?>) method.invoke(null);
204    }
205
206
207    /**
208     * Finds a generator method in the Generators class with the specified parameter types.
209     *
210     * @param methodName     the name of the method to find
211     * @param parameterTypes the parameter types the method should accept
212     * @return the generator method
213     * @throws JUnitException if the method cannot be found
214     */
215    private Method findMethodWithParameters(String methodName, Class<?>... parameterTypes) {
216        try {
217            return Generators.class.getMethod(methodName, parameterTypes);
218        } catch (NoSuchMethodException e) {
219            throw new JUnitException(
220                    "Could not find static generator method '" + methodName +
221                            "' in Generators class with the specified parameter types", e);
222        }
223    }
224}