JUnit4ParameterizedPlan.java

/*******************************************************************************
 * Copyright (c) 2026 Carsten Hammer and others.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *******************************************************************************/
package org.sandbox.jdt.internal.corext.fix.multifile;

import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HexFormat;
import java.util.Map;
import java.util.Objects;

import org.sandbox.jdt.triggerpattern.api.SemanticRewritePlan;
import org.sandbox.jdt.triggerpattern.api.SemanticRewritePlan.NodeKey;

/**
 * Immutable source-discovery evidence for one JUnit 4 Parameterized component.
 * This is a prepared plan, not permission to apply a migration: an executor must
 * still prove its target strategy with the JDT runtime oracle and revalidate the
 * complete source scope. No AST or Java-model elements escape discovery.
 */
public record JUnit4ParameterizedPlan(NodeKey testClass, SemanticRewritePlan semanticPlan,
		Map<NodeKey, String> compilationUnits, Map<String, String> sourceFingerprints) {

	public static final String CONTRACT= "junit4-parameterized"; //$NON-NLS-1$
	public static final String RUNNER_CLASS= "JUNIT4_PARAMETERIZED_CLASS"; //$NON-NLS-1$
	public static final String PROVIDER= "JUNIT4_PARAMETERIZED_PROVIDER"; //$NON-NLS-1$
	public static final String CONSTRUCTOR= "JUNIT4_PARAMETERIZED_CONSTRUCTOR"; //$NON-NLS-1$
	public static final String PARAMETER= "JUNIT4_PARAMETERIZED_PARAMETER"; //$NON-NLS-1$
	public static final String FIELD= "JUNIT4_PARAMETERIZED_FIELD"; //$NON-NLS-1$
	public static final String TEST= "JUNIT4_PARAMETERIZED_TEST"; //$NON-NLS-1$
	public static final String HAS_PROVIDER= "provider"; //$NON-NLS-1$
	public static final String HAS_CONSTRUCTOR= "constructor"; //$NON-NLS-1$
	public static final String HAS_PARAMETER= "constructorParameter"; //$NON-NLS-1$
	public static final String HAS_FIELD= "injectedField"; //$NON-NLS-1$
	public static final String HAS_TEST= "testMethod"; //$NON-NLS-1$
	public static final String DELEGATES_TO= "delegatesTo"; //$NON-NLS-1$
	public static final String HIERARCHY_TYPE= "JUNIT4_PARAMETERIZED_HIERARCHY"; //$NON-NLS-1$
	public static final String TARGET_STRATEGY= "targetStrategy"; //$NON-NLS-1$
	public static final String PARAMETERIZED_CLASS= "PARAMETERIZED_CLASS"; //$NON-NLS-1$

	/** The coordinated planner selects the runtime-verified class strategy explicitly. */
	public boolean executable() {
		return org.sandbox.jdt.triggerpattern.api.SemanticPlanValue.string(PARAMETERIZED_CLASS)
				.equals(semanticPlan.valuesByNode().getOrDefault(testClass, Map.of()).get(TARGET_STRATEGY));
	}

	public JUnit4ParameterizedPlan {
		Objects.requireNonNull(testClass);
		Objects.requireNonNull(semanticPlan);
		if (!semanticPlan.satisfiesContract(CONTRACT)) {
			throw new IllegalArgumentException("Expected the junit4-parameterized contract"); //$NON-NLS-1$
		}
		compilationUnits= Map.copyOf(compilationUnits);
		sourceFingerprints= Map.copyOf(sourceFingerprints);
		if (!semanticPlan.rolesByNode().getOrDefault(testClass, java.util.Set.of()).contains(RUNNER_CLASS)
				|| !compilationUnits.keySet().containsAll(semanticPlan.rolesByNode().keySet())
				|| !sourceFingerprints.keySet().containsAll(compilationUnits.values())) {
			throw new IllegalArgumentException("Incomplete Parameterized source evidence"); //$NON-NLS-1$
		}
	}

	/** Checks exact source text and scope membership, including additions and deletions. */
	public boolean isCurrent(Map<String, String> currentSources) {
		return currentSources != null && currentSources.keySet().equals(sourceFingerprints.keySet())
				&& currentSources.entrySet().stream().allMatch(entry -> entry.getValue() != null
						&& fingerprint(entry.getValue()).equals(sourceFingerprints.get(entry.getKey())));
	}

	static String fingerprint(String source) {
		try {
			return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256") //$NON-NLS-1$
					.digest(source.getBytes(StandardCharsets.UTF_8)));
		} catch (NoSuchAlgorithmException e) {
			throw new IllegalStateException("Java must provide SHA-256", e); //$NON-NLS-1$
		}
	}
}