MethodEntry.java

package org.egothor.methodatlas.gui.model;

import org.egothor.methodatlas.ai.AiMethodSuggestion;
import org.egothor.methodatlas.api.DiscoveredMethod;

import java.util.List;
import java.util.Objects;

/**
 * Mutable view-model for a single discovered test method.
 *
 * <p>The {@link #discovered} field is set once on creation; the
 * {@link #suggestion} field is filled in later by the AI enrichment
 * phase and the {@link #appliedTags} field is updated when the user
 * applies a patch to the source file.</p>
 */
public final class MethodEntry {

    /** Visual state shown in the results tree. */
    public enum TagStatus {
        /** No AI result available. */
        NO_AI,
        /** AI classified this method as not security-relevant. */
        NOT_SECURITY,
        /** AI suggests tags not yet present in the source. */
        NEEDS_REVIEW,
        /** Source tags are consistent with the AI suggestion. */
        OK,
        /**
         * Changes have been staged via the GUI but not yet written to disk.
         * The pending state takes priority over all other statuses until the
         * staged patch is either saved (→ {@link #OK}/{@link #NEEDS_REVIEW})
         * or cleared (→ previous status).
         */
        PENDING_SAVE
    }

    private final DiscoveredMethod discovered;
    private AiMethodSuggestion suggestion;
    private List<String> appliedTags;

    // ── Staged (pending) patch ────────────────────────────────────────────
    private List<String> pendingTags;
    private String pendingDisplayName;

    /**
     * Creates a new entry from a just-discovered method.
     *
     * @param discovered non-null discovered method
     * @param suggestion AI suggestion, may be {@code null}
     */
    public MethodEntry(DiscoveredMethod discovered, AiMethodSuggestion suggestion) {
        this.discovered = Objects.requireNonNull(discovered, "discovered");
        this.suggestion = suggestion;
    }

    /**
     * Returns the immutable discovery record.
     *
     * @return the immutable discovery record
     */
    public DiscoveredMethod discovered() { return discovered; }

    /**
     * Returns the AI suggestion, or {@code null} if not yet available.
     *
     * @return the AI suggestion, or {@code null} if not yet available
     */
    public AiMethodSuggestion suggestion() { return suggestion; }

    /** Updates the AI suggestion (called from the AI enrichment phase). */
    public void setSuggestion(AiMethodSuggestion suggestion) {
        this.suggestion = suggestion;
    }

    /**
     * Returns the tags that were last applied to the source file by this GUI,
     * or {@code null} when no patch has been applied in this session.
     *
     * @return applied tags, or {@code null}
     */
    public List<String> appliedTags() { return appliedTags; }

    /** Records the tags that were written to the source file. */
    public void setAppliedTags(List<String> tags) {
        this.appliedTags = tags == null ? null : List.copyOf(tags);
    }

    /**
     * Returns {@code true} when this entry has a staged (pending) patch that
     * has not yet been written to disk.
     *
     * @return {@code true} if a staged patch exists
     */
    public boolean hasPendingChanges() { return pendingTags != null; }

    /**
     * Returns the tags queued to be written by a future "Save All" operation,
     * or {@code null} if no patch is staged.
     *
     * @return pending tag list, or {@code null}
     */
    public List<String> getPendingTags() { return pendingTags; }

    /**
     * Returns the display name queued to be written by a future "Save All"
     * operation, or {@code null} if none is staged.
     *
     * @return pending display name, or {@code null}
     */
    public String getPendingDisplayName() { return pendingDisplayName; }

    /**
     * Stages a patch for this entry without writing to disk.
     *
     * <p>Calling this method causes {@link #tagStatus()} to return
     * {@link TagStatus#PENDING_SAVE} until the patch is either saved
     * ({@link #clearStagedPatch()} + {@link #setAppliedTags(List)}) or
     * discarded ({@link #clearStagedPatch()}).</p>
     *
     * @param tags        tag list to stage; must not be {@code null}
     * @param displayName display name to stage, or {@code null} to leave unchanged
     */
    public void setStagedPatch(List<String> tags, String displayName) {
        this.pendingTags = List.copyOf(tags);
        this.pendingDisplayName = displayName;
    }

    /**
     * Removes any staged patch without writing to disk.
     *
     * <p>After this call {@link #hasPendingChanges()} returns {@code false}.</p>
     */
    public void clearStagedPatch() {
        this.pendingTags = null;
        this.pendingDisplayName = null;
    }

    /**
     * Computes the visual status for the results tree.
     *
     * <p>Priority: if tags were applied in this session, compare against
     * the applied tags; otherwise compare the source tags against the AI
     * suggestion.</p>
     *
     * @return current tag status
     */
    public TagStatus tagStatus() {
        if (pendingTags != null) {
            return TagStatus.PENDING_SAVE;
        }
        if (suggestion == null) {
            return TagStatus.NO_AI;
        }
        if (!suggestion.securityRelevant()) {
            return TagStatus.NOT_SECURITY;
        }
        List<String> aiTags = suggestion.tags() != null ? suggestion.tags() : List.of();
        if (aiTags.isEmpty()) {
            return TagStatus.OK;
        }
        List<String> reference = appliedTags != null ? appliedTags : discovered.tags();
        for (String aiTag : aiTags) {
            if (!reference.contains(aiTag)) {
                return TagStatus.NEEDS_REVIEW;
            }
        }
        return TagStatus.OK;
    }

    /**
     * Returns the suggested display name from AI, or {@code null} if none.
     *
     * @return the suggested display name from AI, or {@code null}
     */
    public String suggestedDisplayName() {
        return suggestion != null ? suggestion.displayName() : null;
    }

    @Override
    public String toString() {
        return discovered.fqcn() + "#" + discovered.method();
    }
}