SuggestionLookup.java

package org.egothor.methodatlas.ai;

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;

/**
 * Immutable lookup structure providing efficient access to AI-generated method
 * suggestions by method name.
 *
 * <p>
 * This class acts as an adapter between the class-level suggestion model
 * returned by the AI subsystem ({@link AiClassSuggestion}) and the per-method
 * processing logic used by {@code MethodAtlasApp}. It converts the list of
 * {@link AiMethodSuggestion} objects into a name-indexed lookup map so that
 * suggestions can be retrieved in constant time during traversal of parsed test
 * methods.
 * </p>
 *
 * <h2>Design Characteristics</h2>
 *
 * <ul>
 * <li>immutable after construction</li>
 * <li>null-safe for missing or malformed AI responses</li>
 * <li>optimized for repeated method-level lookups</li>
 * </ul>
 *
 * <p>
 * If the AI response contains duplicate suggestions for the same method, only
 * the first occurrence is retained.
 * </p>
 *
 * @see AiClassSuggestion
 * @see AiMethodSuggestion
 */
public final class SuggestionLookup {

    private final Map<String, AiMethodSuggestion> byMethodName;

    /**
     * Creates a new immutable lookup instance backed by the supplied map.
     *
     * <p>
     * The internal map is defensively copied to guarantee immutability of the
     * lookup structure.
     * </p>
     *
     * @param byMethodName mapping from method names to AI suggestions
     */
    private SuggestionLookup(Map<String, AiMethodSuggestion> byMethodName) {
        this.byMethodName = Map.copyOf(byMethodName);
    }

    /**
     * Creates a lookup instance from a class-level AI suggestion result.
     *
     * <p>
     * The method extracts all method suggestions contained in the supplied
     * {@link AiClassSuggestion} and indexes them by method name. Entries with
     * {@code null} suggestions, missing method names, or blank method names are
     * ignored.
     * </p>
     *
     * <p>
     * If the suggestion contains no method entries, an empty lookup instance is
     * returned.
     * </p>
     *
     * @param suggestion AI classification result for a test class
     * @return lookup structure providing fast access to method suggestions
     */
    public static SuggestionLookup from(AiClassSuggestion suggestion) {
        if (suggestion == null || suggestion.methods() == null || suggestion.methods().isEmpty()) {
            return new SuggestionLookup(Map.of());
        }

        Map<String, AiMethodSuggestion> map = new HashMap<>();
        for (AiMethodSuggestion methodSuggestion : suggestion.methods()) {
            if (methodSuggestion == null) {
                continue;
            }
            if (methodSuggestion.methodName() == null || methodSuggestion.methodName().isBlank()) {
                continue;
            }
            map.putIfAbsent(methodSuggestion.methodName(), methodSuggestion);
        }

        return new SuggestionLookup(map);
    }

    /**
     * Retrieves the AI suggestion for the specified method name.
     *
     * <p>
     * If no suggestion exists for the method, an empty {@link Optional} is
     * returned.
     * </p>
     *
     * @param methodName name of the method being queried
     * @return optional containing the suggestion if present
     *
     * @throws NullPointerException if {@code methodName} is {@code null}
     */
    public Optional<AiMethodSuggestion> find(String methodName) {
        Objects.requireNonNull(methodName, "methodName");
        return Optional.ofNullable(byMethodName.get(methodName));
    }
}