JUnitScopeDiagnostics.java

/*******************************************************************************
 * Copyright (c) 2026 Carsten Hammer.
 * SPDX-License-Identifier: EPL-2.0
 *******************************************************************************/
package org.sandbox.jdt.internal.ui.fix;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;

import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IJavaProject;

import org.sandbox.jdt.cleanup.multifile.RelatedCompilationUnitSearch;
import org.sandbox.jdt.internal.corext.fix.multifile.JUnitScopeCandidateDetector.SearchSeeds;

/** Optional stderr observations of decisions already made by the normal scope path. */
final class JUnitScopeDiagnostics {
	private static final String PROPERTY= "sandbox.junit.scope.diagnostics"; //$NON-NLS-1$
	private static final Expansion DISABLED= new Expansion(null, null, null);

	private JUnitScopeDiagnostics() {
	}

	private static boolean enabled() {
		try {
			return Boolean.getBoolean(PROPERTY);
		} catch (RuntimeException error) {
			diagnosticError("property", error); //$NON-NLS-1$
			return false;
		}
	}

	static Expansion expansion(IJavaProject project, Set<String> selected, Set<String> pending) {
		return enabled() ? new Expansion(project, selected, pending) : DISABLED;
	}

	static final class Expansion {
		private final IJavaProject project;
		private final Set<String> selected;
		private final Set<String> pending;
		private List<SearchSeeds> seeds;
		private SearchSeeds merged;

		private Expansion(IJavaProject project, Set<String> selected, Set<String> pending) {
			this.project= project;
			this.selected= selected;
			this.pending= pending;
		}

		void seeds(SearchSeeds standard, SearchSeeds junit3, SearchSeeds lifecycle, SearchSeeds helper,
				SearchSeeds combined) {
			if (project != null) {
				seeds= List.of(standard, junit3, lifecycle, helper);
				merged= combined;
			}
		}

		void finish(String branch, Collection<IJavaProject> projects, Collection<IJavaProject> readOnly,
				Collection<ICompilationUnit> allowed, Collection<ICompilationUnit> required,
				RelatedCompilationUnitSearch.Result related) {
			if (project == null) {
				return;
			}
			try {
				StringBuilder text= new StringBuilder("[sandbox.junit.scope] expansion projectLabel=") //$NON-NLS-1$
						.append(label(project)).append(" branch=").append(branch) //$NON-NLS-1$
						.append(" selected=").append(sorted(selected)).append(" pending=").append(sorted(pending)); //$NON-NLS-1$ //$NON-NLS-2$
				if (seeds != null) {
					List<String> names= List.of("standard", "junit3", "lifecycle", "helper"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$
					for (int i= 0; i < seeds.size(); i++) {
						SearchSeeds seed= seeds.get(i);
						text.append(' ').append(names.get(i)).append("={candidate=").append(seed.candidateFound()) //$NON-NLS-1$
								.append(",complete=").append(seed.complete()).append(",elements=").append(labels(seed.elements())) //$NON-NLS-1$ //$NON-NLS-2$
								.append(",direct=").append(labels(seed.directCompilationUnits())).append('}'); //$NON-NLS-1$
					}
					text.append(" mergedCandidate=").append(merged.candidateFound()).append(" mergedComplete=").append(merged.complete()); //$NON-NLS-1$ //$NON-NLS-2$
				}
				text.append(" projectLabels=").append(labels(projects)).append(" readOnlyLabels=").append(labels(readOnly)) //$NON-NLS-1$ //$NON-NLS-2$
						.append(" allowedLabels=").append(labels(allowed)).append(" requiredLabels=").append(labels(required)); //$NON-NLS-1$ //$NON-NLS-2$
				if (related != null) {
					text.append(" relatedComplete=").append(related.complete()).append(" relatedUnitLabels=").append(labels(related.compilationUnits())) //$NON-NLS-1$ //$NON-NLS-2$
							.append(" relatedReasons=").append(related.rejectionReasons()); //$NON-NLS-1$
				}
				System.err.println(text);
			} catch (RuntimeException error) {
				diagnosticError("expansion", error); //$NON-NLS-1$
			}
		}
	}

	static void consumed(IJavaProject project, ICompilationUnit[] units, Set<String> selected,
			Set<String> expected, Set<String> pending, Boolean decision, String branch) {
		if (!enabled()) {
			return;
		}
		try {
			Set<String> missing= expected == null || selected == null ? null : new TreeSet<>(expected);
			if (missing != null) {
				missing.removeAll(selected);
			}
			System.err.println("[sandbox.junit.scope] consume projectLabel=" + label(project) //$NON-NLS-1$
					+ " branch=" + branch + " decision=" + decision + " selected=" //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
					+ (selected == null ? "not-computed" : sorted(selected)) //$NON-NLS-1$
					+ " expected=" + sorted(expected) + " pending=" + sorted(pending) + " missingExpected=" + missing //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
					+ " unitLabels=" + labels(List.of(units))); //$NON-NLS-1$
		} catch (RuntimeException error) {
			diagnosticError("consume", error); //$NON-NLS-1$
		}
	}

	private static List<String> labels(Collection<? extends IJavaElement> elements) {
		if (elements == null) {
			return null;
		}
		List<String> result= new ArrayList<>();
		for (IJavaElement element : elements) {
			result.add(label(element));
		}
		Collections.sort(result);
		return result;
	}

	private static String label(IJavaElement element) {
		StringBuilder result= new StringBuilder();
		// Do not create handle mementos: a root memento can resolve its classpath entry.
		for (IJavaElement current= element; current != null; current= current.getParent()) {
			if (result.length() > 0) {
				result.append('/');
			}
			result.append(current.getClass().getName()).append('(').append(current.getElementName()).append(')');
		}
		return result.toString();
	}

	private static void diagnosticError(String phase, RuntimeException error) {
		System.err.println("[sandbox.junit.scope] diagnostic-error phase=" + phase //$NON-NLS-1$
				+ " type=" + error.getClass().getName()); //$NON-NLS-1$
	}

	private static Set<String> sorted(Set<String> handles) {
		return handles == null ? null : new TreeSet<>(handles);
	}
}