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.TypedGenerator;
019import org.junit.jupiter.api.extension.ExtensionContext;
020import org.junit.jupiter.params.provider.Arguments;
021import org.junit.jupiter.params.support.AnnotationConsumer;
022import org.junit.platform.commons.JUnitException;
023import org.junit.platform.commons.util.ReflectionUtils;
024
025import java.lang.reflect.Method;
026import java.util.Arrays;
027import java.util.List;
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 a factory class for parameterized tests
035 * annotated with {@link TypeGeneratorFactorySource}.
036 * 
037 * <p>
038 * This provider invokes the specified factory method 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 * @since 2.0
050 * @see TypeGeneratorFactorySource
051 * @see TypedGenerator
052 */
053public class TypeGeneratorFactoryArgumentsProvider extends AbstractTypedGeneratorArgumentsProvider
054        implements AnnotationConsumer<TypeGeneratorFactorySource> {
055
056    private Class<?> factoryClass;
057    private String factoryMethod;
058    private String[] methodParameters;
059    private int count;
060
061    @Override
062    public void accept(TypeGeneratorFactorySource annotation) {
063        factoryClass = annotation.factoryClass();
064        factoryMethod = annotation.factoryMethod();
065        methodParameters = annotation.methodParameters();
066        count = Math.max(1, annotation.count());
067    }
068
069    @Override
070    protected Stream<? extends Arguments> provideArgumentsForGenerators(ExtensionContext context) {
071        // Create generator instance using factory
072        var generator = createGeneratorFromFactory();
073
074        // Generate values
075        return generateArguments(generator).stream();
076    }
077
078    @Override
079    protected long getSeed() {
080        return -1L;
081    }
082
083    @Override
084    protected int getCount() {
085        return count;
086    }
087
088    /**
089     * Creates a TypedGenerator instance by invoking the specified factory method.
090     * 
091     * @return a TypedGenerator instance
092     * @throws JUnitException if the factory method cannot be found or invoked
093     */
094    private TypedGenerator<?> createGeneratorFromFactory() {
095        requireNonNull(factoryClass, "Factory class must not be null");
096        requireNonNull(factoryMethod, "Factory method must not be null");
097
098        try {
099            // Find the factory method
100            Method method = findFactoryMethod();
101
102            // Invoke the factory method with the provided parameters
103            return (TypedGenerator<?>) method.invoke(null, (Object[]) methodParameters);
104        } catch (Exception e) {
105            throw new JUnitException(
106                    "Failed to create TypedGenerator using factory method '" + factoryMethod +
107                            "' in class '" + factoryClass.getName() + "'", e);
108        }
109    }
110
111    /**
112     * Finds the factory method in the factory class.
113     * 
114     * @return the factory method
115     * @throws JUnitException if the method cannot be found
116     */
117    private Method findFactoryMethod() {
118        // Look for a method with the exact parameter count
119        List<Method> candidateMethods = Arrays.stream(factoryClass.getMethods())
120                .filter(m -> m.getName().equals(factoryMethod))
121                .filter(ReflectionUtils::isStatic)
122                .filter(m -> m.getParameterCount() == methodParameters.length)
123                .filter(m -> TypedGenerator.class.isAssignableFrom(m.getReturnType()))
124                .toList();
125
126        if (candidateMethods.isEmpty()) {
127            throw new JUnitException(
128                    "Could not find static factory method '" + factoryMethod +
129                            "' in class '" + factoryClass.getName() +
130                            "' with " + methodParameters.length + " parameters that returns TypedGenerator");
131        }
132
133        if (candidateMethods.size() > 1) {
134            // If multiple methods match, try to find one that accepts String parameters
135            var stringParamMethods = candidateMethods.stream()
136                    .filter(m -> Arrays.stream(m.getParameterTypes())
137                            .allMatch(p -> p == String.class))
138                    .toList();
139
140            if (stringParamMethods.size() == 1) {
141                return stringParamMethods.getFirst();
142            }
143        }
144
145        // If we have exactly one candidate or couldn't narrow down further, use the first one
146        return candidateMethods.getFirst();
147    }
148}