/*
 * $Id$
 *
 * SARL is an general-purpose agent programming language.
 * More details on http://www.sarl.io
 *
 * Copyright (C) 2014-2026 SARL.io, the original authors and main authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package io.sarl.docs.generator;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.net.Proxy.Type;
import java.net.ProxySelector;
import java.net.SocketAddress;
import java.net.SocketTimeoutException;
import java.net.URL;
import java.net.URLConnection;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.plugins.annotations.ResolutionScope;
import org.arakhne.afc.vmutil.FileSystem;
import org.eclipse.xtext.generator.trace.LocationData;
import org.eclipse.xtext.generator.trace.TraceRegionSerializer;
import org.eclipse.xtext.util.JavaVersion;
import org.eclipse.xtext.util.Strings;
import org.eclipse.xtext.xbase.compiler.ImportManager;
import org.eclipse.xtext.xbase.compiler.output.FakeTreeAppendable;
import org.eclipse.xtext.xbase.compiler.output.ITreeAppendable;
import org.junit.Assume;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Assumptions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.opentest4j.AssertionFailedError;
import org.opentest4j.TestAbortedException;

import com.google.common.base.Throwables;
import com.google.common.collect.Lists;
import com.google.inject.Injector;

import io.sarl.docs.generator.markdown.MarkdownParser;
import io.sarl.docs.generator.parser.AbstractMarkerLanguageParser;
import io.sarl.docs.generator.parser.DynamicValidationComponent;
import io.sarl.docs.generator.parser.DynamicValidationContext;
import io.sarl.docs.generator.parser.ValidationComponent;
import io.sarl.docs.validator.DocumentationSetup;
import io.sarl.docs.validator.ScriptExecutor;
import io.sarl.lang.core.SARLVersion;
import io.sarl.lang.util.Utils;

/** Maven MOJO that is generating the documentation tests for the SARL project.
 *
 * @author $Author: sgalland$
 * @version $FullVersion$
 * @mavengroupid $GroupId$
 * @mavenartifactid $ArtifactId$
 * @since 0.6
 */
@Mojo(name = "generatetests", defaultPhase = LifecyclePhase.GENERATE_TEST_SOURCES,
	requiresDependencyResolution = ResolutionScope.TEST)
public class GenerateTestsMojo extends AbstractDocumentationMojo {

	private static final String BASE_PACKAGE = "io.sarl.docs.tests"; //$NON-NLS-1$

	/**
	 * Indicates if the references to the local files should be validated.
	 */
	@Parameter(defaultValue = "true", required = false)
	protected boolean localLinkValidation;

	/**
	 * Indicates if the references to the remote Internet pages should be validated.
	 */
	@Parameter(defaultValue = "true", required = false)
	protected boolean remoteLinkValidation;

	/**
	 * Indicates if the timeout to use for connecting to remote hosts.
	 * This timeout value is expressed in milliseconds.
	 */
	@Parameter(defaultValue = "5000", required = false)
	protected int remoteLinkTimeOut;

	/**
	 * Indicates if a timeout for connecting to remote hosts should be ignored.
	 * If the timeout is ignore, the corresponding test will be marked as "ignored".
	 *
	 * @see Assume
	 */
	@Parameter(defaultValue = "true", required = false)
	protected boolean ignoreRemoteLinkTimeOut;

	/**
	 * Indicates if the references to the local files should be validated.
	 */
	@Parameter(defaultValue = "true", required = false)
	protected boolean localImageValidation;

	private TraceRegionSerializer traceSerializer;

	/** Replies the trace serializer.
	 *
	 * @return the trace serializer.
	 */
	protected TraceRegionSerializer getTraceSerializer() {
		if (this.traceSerializer == null) {
			this.traceSerializer = this.injector.getInstance(TraceRegionSerializer.class);
		}
		return this.traceSerializer;
	}

	@Override
	protected String getSkippingMessage() {
		final String[] variableNames = {
			"maven.test.skip", //$NON-NLS-1$
			"skipTests", //$NON-NLS-1$
			"io.sarl.docs.tests.skip", //$NON-NLS-1$
		};
		String value = null;
		var i = 0;
		while (Strings.isEmpty(value) && i < variableNames.length) {
			value = System.getProperty(variableNames[i]);
			++i;
		}
		i = 0;
		while (Strings.isEmpty(value) && i < variableNames.length) {
			value = System.getenv(variableNames[i]);
			++i;
		}
		if (Boolean.parseBoolean(value)) {
			return "Tests are skipped."; //$NON-NLS-1$
		}
		return null;
	}

	@Override
	protected AbstractMarkerLanguageParser createLanguageParser(File inputFile) throws MojoExecutionException, IOException {
		final var parser = super.createLanguageParser(inputFile);
		if (parser instanceof MarkdownParser mdParser) {
			mdParser.setLocalImageReferenceValidation(this.localImageValidation);
			mdParser.setLocalFileReferenceValidation(this.localLinkValidation);
			mdParser.setRemoteReferenceValidation(
					!this.session.isOffline() && this.remoteLinkValidation);
		}
		return parser;
	}

	@Override
	protected String internalExecute(Map<File, File> files) {
		getLog().info(Messages.GenerateTestsMojo_11);
		final var output = FileSystem.convertStringToFile(this.testSourceDirectory);
		final var msg = internalExecute(files, output);
		if (!Strings.isEmpty(msg)) {
			return msg;
		}

		try {
			generateAbstractTest(output);
		} catch (IOException exception) {
			final var message = Throwables.getRootCause(exception).getLocalizedMessage();
			getLog().error(message);
			getLog().debug(exception);
			return message;
		}

		return null;
	}

	@Override
	protected String internalExecute(Map<File, File> files, File outputFolder) {
		try {
			FileSystem.delete(outputFolder);
		} catch (IOException exception) {
			final var message = formatErrorMessage(outputFolder, exception);
			getLog().error(message);
			getLog().debug(exception);
			return message;
		}
		return super.internalExecute(files, outputFolder);
	}

	@SuppressWarnings({"checkstyle:npathcomplexity", "checkstyle:cyclomaticcomplexity", "all"})
	@Override
	protected void internalExecute(File sourceFolder, File inputFile, File relativeInputFile, File outputFolder,
			AbstractMarkerLanguageParser parser) throws IOException {
		getLog().debug(MessageFormat.format(Messages.GenerateTestsMojo_0, inputFile.getName()));
		final var successCompilationComponents = new ArrayList<ValidationComponent>();
		final var failureCompilationComponents = new ArrayList<ValidationComponent>();
		final var factualComponents = new ArrayList<ValidationComponent>();
		for (final var component : parser.getStandardValidationComponents(inputFile)) {
			if (component.isCompilable()) {
				if (component.isExecutable()) {
					factualComponents.add(component);
				} else {
					successCompilationComponents.add(component);
				}
			} else {
				failureCompilationComponents.add(component);
			}
		}

		final var validationContext = new DynamicValidationContext();
		validationContext.setSourceRoots(this.session.getCurrentProject().getCompileSourceRoots());
		validationContext.setResourceRoots(Lists.transform(this.session.getCurrentProject().getResources(),
				it -> it.getDirectory()));
		validationContext.setDestinationRoots(
				Collections.singletonList(this.session.getCurrentProject().getBuild().getOutputDirectory()));
		final var specificComponents = parser.getMarkerSpecificValidationComponents(
				inputFile, sourceFolder, validationContext);
		if (successCompilationComponents.isEmpty() && failureCompilationComponents.isEmpty()
				&& factualComponents.isEmpty() && specificComponents.isEmpty()) {
			return;
		}

		// Do not change the "Test" postfix because it is used by Surefire for detecting test classes.
		final var basicTestName = toTestName(inputFile);
		final var generalTestName = basicTestName + "Test"; //$NON-NLS-1$

		final var importManager = new ImportManager();
		final var it = new TraceableTreeAppendable(importManager);

		final var displayName = toClassDisplayName(relativeInputFile, basicTestName, generalTestName);
		
		it.append("@").append(DisplayName.class).append("(\"").append(displayName).append("\")").newLine(); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
		it.append("@").append(Tag.class).append("(\"documentation\")").newLine(); //$NON-NLS-1$ //$NON-NLS-2$
		it.append("@").append(Tag.class).append("(\"doc\")").newLine(); //$NON-NLS-1$ //$NON-NLS-2$
		it.append("public class ").append(generalTestName).append(" extends ") //$NON-NLS-1$ //$NON-NLS-2$
			.append(BASE_PACKAGE).append(".AbstractBaseTest").append(" {").increaseIndentation().newLine(); //$NON-NLS-1$ //$NON-NLS-2$
		
		final var absoluteInputFile = FileSystem.makeAbsolute(relativeInputFile, sourceFolder);
		
		generateTestsForSuccessCode(it, true, importManager, relativeInputFile, successCompilationComponents);
		generateTestsForFailureCode(it, true, importManager, relativeInputFile, failureCompilationComponents);
		generateTestsForFacts(it, importManager, absoluteInputFile , factualComponents);
		generateDynamicTests(it, importManager, relativeInputFile, specificComponents);

		it.decreaseIndentation().newLine().append("}").newLine(); //$NON-NLS-1$

		final var packagePath = relativeInputFile.getParentFile();
		final var packageName = toPackageName("docs", packagePath); //$NON-NLS-1$
		write(outputFolder, packageName, generalTestName, importManager, it);

		//generateTraceFile(outputFolder, packageName, generalTestName, it.getTraceRegions());
	}

	private void generateTestsForSuccessCode(ITreeAppendable parent, boolean isDeprecationasError, ImportManager importManager,
			File inputFile, List<ValidationComponent> successCompilationComponents) {
		var i = 0;
		for (final var component : successCompilationComponents) {
			getLog().debug(MessageFormat.format(Messages.GenerateTestsMojo_1,
					inputFile.getName(), Integer.valueOf(component.getLinenoInSourceFile()), component.getCode()));
			final var actionName = toActionName("success", component, i); //$NON-NLS-1$
			final var displayName = toTestDisplayName(Messages.GenerateTestsMojo_7, i, component);
			final var location = new LocationData(
					// Offset
					component.getOffsetInSourceFile(),
					// Length
					component.getLengthInSourceFile(),
					// Line number
					component.getLinenoInSourceFile(),
					// End line number
					component.getEndLinenoInSourceFile(),
					// Source URI
					null);
			final var it = parent.trace(location);
			//
			it.append("@").append(Test.class).newLine(); //$NON-NLS-1$
			it.append("@").append(DisplayName.class).append("(\"").append(displayName).append("\")").newLine(); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
			it.append("@").append(Tag.class).append("(\"success\")").newLine(); //$NON-NLS-1$ //$NON-NLS-2$
			it.append("@").append(Tag.class).append("(\"success_").append(Integer.toString(i)).append("\")").newLine(); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
			it.append("public void ").append(actionName).append("() throws ") //$NON-NLS-1$ //$NON-NLS-2$
				.append(Exception.class).append(" {").increaseIndentation().newLine(); //$NON-NLS-1$
			it.append(ScriptExecutor.class).append(" scriptExecutor = getScriptExecutor();").newLine(); //$NON-NLS-1$
			var deprecationError = isDeprecationasError;
			final var componentDeprecationError = component.isDeprecationIssuesAsErrors();
			if (componentDeprecationError != null) {
				deprecationError = componentDeprecationError.booleanValue();
			}
			it.append("scriptExecutor.setDeprecationAsError(").append(Boolean.toString(deprecationError)).append(");").newLine(); //$NON-NLS-1$ //$NON-NLS-2$
			it.append(List.class).append("<String> issues = scriptExecutor.compile(") //$NON-NLS-1$
				.append(str(Integer.valueOf(component.getLinenoInSourceFile()))).append(", \"") //$NON-NLS-1$
				.append(str(component.getCode())).append("\");").newLine(); //$NON-NLS-1$
			it.append("assertNoIssue(").append(str(Integer.valueOf(component.getLinenoInSourceFile()))) //$NON-NLS-1$
				.append(", issues);").decreaseIndentation().newLine(); //$NON-NLS-1$
			it.append("}").newLine(); //$NON-NLS-1$
			//
			++i;
		}
	}

	private void generateTestsForFailureCode(ITreeAppendable parent, boolean isDeprecationasError, ImportManager importManager,
			File inputFile, List<ValidationComponent> failureCompilationComponents) {
		var i = 0;
		for (final var component : failureCompilationComponents) {
			getLog().debug(MessageFormat.format(Messages.GenerateTestsMojo_2,
					inputFile.getName(), Integer.valueOf(component.getLinenoInSourceFile()), component.getCode()));
			final var actionName = toActionName("failure", component, i); //$NON-NLS-1$
			final var displayName = toTestDisplayName(Messages.GenerateTestsMojo_8, i, component);
			final var location = new LocationData(
					// Offset
					component.getOffsetInSourceFile(),
					// Length
					component.getLengthInSourceFile(),
					// Line number
					component.getLinenoInSourceFile(),
					// End line number
					component.getEndLinenoInSourceFile(),
					// Source URI
					null);
			final var it = parent.trace(location);
			//
			it.append("@").append(Test.class).newLine(); //$NON-NLS-1$
			it.append("@").append(DisplayName.class).append("(\"").append(displayName).append("\")").newLine(); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
			it.append("@").append(Tag.class).append("(\"failure\")").newLine(); //$NON-NLS-1$ //$NON-NLS-2$
			it.append("@").append(Tag.class).append("(\"failure_").append(Integer.toString(i)).append("\")").newLine(); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
			it.append("public void ").append(actionName).append("() throws ") //$NON-NLS-1$ //$NON-NLS-2$
				.append(Exception.class).append(" {").increaseIndentation().newLine(); //$NON-NLS-1$
			it.append(ScriptExecutor.class).append(" scriptExecutor = getScriptExecutor();").newLine(); //$NON-NLS-1$
			var deprecationError = isDeprecationasError;
			final var componentDeprecationError = component.isDeprecationIssuesAsErrors();
			if (componentDeprecationError != null) {
				deprecationError = componentDeprecationError.booleanValue();
			}
			it.append("scriptExecutor.setDeprecationAsError(").append(Boolean.toString(deprecationError)).append(");").newLine(); //$NON-NLS-1$ //$NON-NLS-2$
			it.append(List.class).append("<String> issues = scriptExecutor.compile(") //$NON-NLS-1$
				.append(str(Integer.valueOf(component.getLinenoInSourceFile()))).append(", \"") //$NON-NLS-1$
				.append(str(component.getCode())).append("\");").newLine(); //$NON-NLS-1$
			it.append("assertIssues(").append(str(Integer.valueOf(component.getLinenoInSourceFile()))) //$NON-NLS-1$
				.append(", issues);").decreaseIndentation().newLine(); //$NON-NLS-1$
			it.append("}").newLine(); //$NON-NLS-1$
			//
			++i;
		}
	}

	private void generateTestsForFacts(ITreeAppendable parent, ImportManager importManager,
			File absoluteInputFile, List<ValidationComponent> factualComponents) {
		var i = 0;
		for (final var component : factualComponents) {
			getLog().debug(MessageFormat.format(Messages.GenerateTestsMojo_3,
					absoluteInputFile.getName(), Integer.valueOf(component.getLinenoInSourceFile()), component.getCode()));
			final var actionName = toActionName("fact", component, i); //$NON-NLS-1$
			final var displayName = toTestDisplayName(Messages.GenerateTestsMojo_9, i, component);
			final var location = new LocationData(
					// Offset
					component.getOffsetInSourceFile(),
					// Length
					component.getLengthInSourceFile(),
					// Line number
					component.getLinenoInSourceFile(),
					// End line number
					component.getEndLinenoInSourceFile(),
					// Source URI
					null);
			final var it = parent.trace(location);
			//
			it.append("@").append(Test.class).newLine(); //$NON-NLS-1$
			it.append("@").append(DisplayName.class).append("(\"").append(displayName).append("\")").newLine(); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
			it.append("@").append(Tag.class).append("(\"fact\")").newLine(); //$NON-NLS-1$ //$NON-NLS-2$
			it.append("@").append(Tag.class).append("(\"fact_").append(Integer.toString(i)).append("\")").newLine(); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
			it.append("public void ").append(actionName).append("() throws ") //$NON-NLS-1$ //$NON-NLS-2$
				.append(Exception.class).append(" {").increaseIndentation().newLine(); //$NON-NLS-1$
			it.append(System.class).append(".setProperty(") //$NON-NLS-1$
				.append(ScriptExecutor.class).append(".PROP_CURRENT_FILE, \"") //$NON-NLS-1$
				.append(Strings.convertToJavaString(absoluteInputFile.getAbsolutePath(), false)).append("\");").newLine(); //$NON-NLS-1$
			it.append(System.class).append(".setProperty(") //$NON-NLS-1$
				.append(ScriptExecutor.class).append(".PROP_CURRENT_FOLDER, \"") //$NON-NLS-1$
				.append(Strings.convertToJavaString(absoluteInputFile.getParentFile().getAbsolutePath(), false)).append("\");").newLine(); //$NON-NLS-1$
			it.append("final String expected = ").append(Utils.class).append(".dump(") //$NON-NLS-1$ //$NON-NLS-2$
				.append(Boolean.class).append(".TRUE, false) + \"\\nOR\\nObject {\\n}\\n\";").newLine(); //$NON-NLS-1$
			it.append("Object result;").newLine(); //$NON-NLS-1$
			it.append("try {").increaseIndentation().newLine(); //$NON-NLS-1$
			it.append("result = getScriptExecutor().execute(").append(str(Integer.valueOf(component.getLinenoInSourceFile()))) //$NON-NLS-1$
				.append(", \"").append(str(component.getCode())).append("\");").decreaseIndentation().newLine(); //$NON-NLS-1$ //$NON-NLS-2$
			it.append("} catch (").append(Throwable.class).append(" exception) {").increaseIndentation().newLine(); //$NON-NLS-1$ //$NON-NLS-2$
			it.append("throw new ").append(AssertionFailedError.class) //$NON-NLS-1$
				.append("(exception.getLocalizedMessage() + \" [line: ") //$NON-NLS-1$
				.append(str(Integer.valueOf(component.getLinenoInSourceFile()))).append("]\", expected, ") //$NON-NLS-1$
				.append(Throwables.class).append(".getStackTraceAsString(exception));").decreaseIndentation().newLine(); //$NON-NLS-1$
			it.append("}").newLine(); //$NON-NLS-1$
			it.append("if (result instanceof ").append(Boolean.class).append(" $c$value) {").increaseIndentation().newLine(); //$NON-NLS-1$ //$NON-NLS-2$
			it.append("boolean boolResult = $c$value.booleanValue();").newLine(); //$NON-NLS-1$
			it.append("if (!boolResult) {").increaseIndentation().newLine(); //$NON-NLS-1$
			it.append("throw new ").append(AssertionFailedError.class) //$NON-NLS-1$
				.append("(\"Invalid expression result [line: ").append(str(Integer.valueOf(component.getLinenoInSourceFile()))) //$NON-NLS-1$
				.append("]\", expected, ").append(Utils.class).append(".dump(result, false));").decreaseIndentation().newLine(); //$NON-NLS-1$ //$NON-NLS-2$
			it.append("}").decreaseIndentation().newLine(); //$NON-NLS-1$
			it.append("} else if (result == null || result instanceof ").append(Exception.class) //$NON-NLS-1$
				.append(") {").increaseIndentation().newLine(); //$NON-NLS-1$
			it.append("throw new ").append(AssertionFailedError.class) //$NON-NLS-1$
				.append("(\"Invalid expression result [line: ").append(str(Integer.valueOf(component.getLinenoInSourceFile()))) //$NON-NLS-1$
				.append("]\", expected, ").append(Utils.class).append(".dump(result, false));").decreaseIndentation().newLine(); //$NON-NLS-1$ //$NON-NLS-2$
			it.append("}").decreaseIndentation().newLine(); //$NON-NLS-1$
			it.append("}").newLine(); //$NON-NLS-1$
			//
			++i;
		}
	}

	private void generateDynamicTests(ITreeAppendable parent, ImportManager importManager,
			File inputFile, List<DynamicValidationComponent> specificComponents) {
		var i = 0;
		for (final var component : specificComponents) {
			getLog().debug(MessageFormat.format(Messages.GenerateTestsMojo_4,
					inputFile.getName(), component.functionName() + i));
			final var actionName = toActionName("dyn_" + component.functionName(), component, i); //$NON-NLS-1$
			final var displayName = toTestDisplayName(Messages.GenerateTestsMojo_10, i, component);
			final var location = new LocationData(
					// Offset
					component.getOffsetInSourceFile(),
					// Length
					component.getLengthInSourceFile(),
					// Line number
					component.getLinenoInSourceFile(),
					// End line number
					component.getEndLinenoInSourceFile(),
					// Source URI
					null);
			final var it = parent.trace(location);
			//
			it.append("@").append(Test.class).newLine(); //$NON-NLS-1$
			it.append("@").append(DisplayName.class).append("(\"").append(displayName).append("\")").newLine(); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
			it.append("@").append(Tag.class).append("(\"other\")").newLine(); //$NON-NLS-1$ //$NON-NLS-2$
			it.append("@").append(Tag.class).append("(\"other_").append(Integer.toString(i)).append("\")").newLine(); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
			it.append("public void ").append(actionName).append("() throws ") //$NON-NLS-1$ //$NON-NLS-2$
				.append(Exception.class).append(" {").increaseIndentation().newLine(); //$NON-NLS-1$
			component.generateValidationCode(it);
			it.decreaseIndentation().newLine();
			it.append("}").newLine(); //$NON-NLS-1$
			//
			++i;
		}
	}

	/*private void generateTraceFile(File root, String packageName, String typeName, Collection<AbstractTraceRegion> regions) throws IOException {
		if (!regions.isEmpty()) {
			File folderFile = getOutputFolder(root, packageName);
			File outputFile = getOutputTraceFilename(folderFile, typeName);
			getLog().debug(MessageFormat.format(Messages.GenerateTestsMojo_6, outputFile.getName()));
			folderFile.mkdirs();
			try (final FileOutputStream os = new FileOutputStream(outputFile)) {
				for (final AbstractTraceRegion region : regions) {
					getTraceSerializer().writeTraceRegionTo(region, os);
				}
			}
		}
	}*/

	@SuppressWarnings("removal")
	private void generateAbstractTest(File outputFolder) throws IOException {
		getLog().debug(Messages.GenerateTestsMojo_5);
		final var importManager = new ImportManager();
		final var it = new FakeTreeAppendable(importManager);

		it.append("@").append(SuppressWarnings.class).append("(\"all\")").newLine(); //$NON-NLS-1$ //$NON-NLS-2$
		it.append("public class AbstractBaseTest {").increaseIndentation().newLine(); //$NON-NLS-1$
		it.append("protected static String STR_SUCCESS = \"success\";").newLine(); //$NON-NLS-1$
		it.append("protected static String STR_FAILURE = \"failure\";").newLine(); //$NON-NLS-1$
		it.append("protected static String STR_FACT = \"fact\";").newLine(); //$NON-NLS-1$
		it.append("private static ").append(Injector.class).append(" injector = ") //$NON-NLS-1$ //$NON-NLS-2$
			.append(DocumentationSetup.class).append(".doSetup();").newLine(); //$NON-NLS-1$
		it.append("private ").append(ScriptExecutor.class).append(" scriptExecutor;").newLine(); //$NON-NLS-1$ //$NON-NLS-2$

		it.append("protected ").append(ScriptExecutor.class).append(" getScriptExecutor() {").increaseIndentation().newLine(); //$NON-NLS-1$ //$NON-NLS-2$
		it.append("if (this.scriptExecutor == null) {").increaseIndentation().newLine(); //$NON-NLS-1$
		it.append("this.scriptExecutor = this.injector.getInstance(").append(ScriptExecutor.class).append(".class);").newLine(); //$NON-NLS-1$ //$NON-NLS-2$

		final var cp = new StringBuilder();
		for (final var cpElement : getClassPath()) {
			if (cp.length() > 0) {
				cp.append(File.pathSeparator);
			}
			cp.append(cpElement.getAbsolutePath());
		}
		it.append("scriptExecutor.setClassPath(\"").append(str(cp)).append("\");").newLine(); //$NON-NLS-1$ //$NON-NLS-2$

		final var mp = new StringBuilder();
		for (final var mpElement : getModulePath()) {
			if (mp.length() > 0) {
				mp.append(File.pathSeparator);
			}
			mp.append(mpElement.getAbsolutePath());
		}
		it.append("scriptExecutor.setModulePath(\"").append(str(mp)).append("\");").newLine(); //$NON-NLS-1$ //$NON-NLS-2$

		final var bootPath = getBootClassPath();
		if (!Strings.isEmpty(bootPath)) {
			it.append("scriptExecutor.setBootClassPath(\"") //$NON-NLS-1$
				.append(str(bootPath)).append("\");") //$NON-NLS-1$
				.newLine();
		}
		JavaVersion version = null;
		if (!Strings.isEmpty(this.source)) {
			try {
				version = JavaVersion.fromQualifier(this.source);
			} catch (Throwable exception) {
				version = null;
			}
		}
		if (version == null) {
			try {
				version = JavaVersion.fromQualifier(SARLVersion.MINIMAL_JDK_VERSION_FOR_SARL_COMPILATION_ENVIRONMENT);
			} catch (Throwable exception) {
				version = null;
			}
		}
		if (version == null) {
			throw new IllegalStateException("unknown Java version"); //$NON-NLS-1$
		}
		it.append("scriptExecutor.setJavaSourceVersion(\"").append(version.getQualifier()).append("\");").newLine(); //$NON-NLS-1$ //$NON-NLS-2$

		it.append("scriptExecutor.setTempFolder(new ").append(File.class).append("(\"") //$NON-NLS-1$ //$NON-NLS-2$
		.append(str(this.tempDirectory.getAbsolutePath())).append("\"));") //$NON-NLS-1$
		.decreaseIndentation().newLine();

		it.append("}").newLine(); //$NON-NLS-1$
		it.append("return this.scriptExecutor;").decreaseIndentation().newLine(); //$NON-NLS-1$
		it.append("}").newLine(); //$NON-NLS-1$

		it.append("public void assertNoIssue(int lineno, ").append(List.class).append("<String> issues) {") //$NON-NLS-1$ //$NON-NLS-2$
		.increaseIndentation().newLine();
		it.append("if (issues != null && !issues.isEmpty()) {").increaseIndentation().newLine(); //$NON-NLS-1$
		it.append(StringBuilder.class).append(" msg = new ").append(StringBuilder.class).append("();").newLine(); //$NON-NLS-1$ //$NON-NLS-2$
		it.append("for (String message : issues) {").increaseIndentation().newLine(); //$NON-NLS-1$
		it.append("msg.append(message).append(\"\\n\");").decreaseIndentation().newLine(); //$NON-NLS-1$
		it.append("}").newLine(); //$NON-NLS-1$
		it.append("throw new ").append(AssertionFailedError.class) //$NON-NLS-1$
		.append("(\"Expecting no issue but find one [line:\" + lineno + \"]\\n\" + msg, \"\", msg.toString());") //$NON-NLS-1$
		.decreaseIndentation().newLine();
		it.append("}").decreaseIndentation().newLine(); //$NON-NLS-1$
		it.append("}").newLine(); //$NON-NLS-1$

		it.append("public void assertIssues(int lineno, ").append(List.class).append("<String> issues) {") //$NON-NLS-1$ //$NON-NLS-2$
		.increaseIndentation().newLine();
		it.append("if (issues == null || issues.isEmpty()) {").increaseIndentation().newLine(); //$NON-NLS-1$
		it.append(Assertions.class).append(".fail(\"Expecting issues but did not find one [line:\" + lineno + \"]\");") //$NON-NLS-1$
		.decreaseIndentation().newLine();
		it.append("}").decreaseIndentation().newLine(); //$NON-NLS-1$
		it.append("}").newLine(); //$NON-NLS-1$

		it.append("public String computeHeaderIdWithSectionNumber(String header) {").increaseIndentation().newLine(); //$NON-NLS-1$
		it.append("String id = header.replaceAll(\"[^a-zA-Z0-9]+\", \"-\");").newLine(); //$NON-NLS-1$
		it.append("id = id.toLowerCase();").newLine(); //$NON-NLS-1$
		it.append("id = id.replaceFirst(\"^[^a-zA-Z0-9]+\", \"\");").newLine(); //$NON-NLS-1$
		it.append("id = id.replaceFirst(\"[^a-zA-Z0-9]+$\", \"\");").newLine(); //$NON-NLS-1$
		it.append("if (").append(Strings.class).append(".isEmpty(id)) {").increaseIndentation().newLine(); //$NON-NLS-1$ //$NON-NLS-2$
		it.append("return \"section\";").decreaseIndentation().newLine(); //$NON-NLS-1$
		it.append("}").newLine(); //$NON-NLS-1$
		it.append("return id;").decreaseIndentation().newLine(); //$NON-NLS-1$
		it.append("}").newLine(); //$NON-NLS-1$

		it.append("public String computeHeaderIdWithoutSectionNumber(String header) {").increaseIndentation().newLine(); //$NON-NLS-1$
		it.append("String id = computeHeaderIdWithSectionNumber(header);").newLine(); //$NON-NLS-1$
		it.append("id = id.replaceFirst(\"^[0-9.\\\\-]+\", \"\");").newLine(); //$NON-NLS-1$
		it.append("return id;").decreaseIndentation().newLine(); //$NON-NLS-1$
		it.append("}").newLine(); //$NON-NLS-1$

		it.append("public static String getHttpCodeExplanation(int code) {").increaseIndentation().newLine(); //$NON-NLS-1$
		it.append("switch (code) {").increaseIndentation().newLine(); //$NON-NLS-1$
		it.append("case ").append(HttpURLConnection.class).append(".HTTP_BAD_METHOD: return \"Method Not Allowed\";").newLine(); //$NON-NLS-1$ //$NON-NLS-2$
		it.append("case ").append(HttpURLConnection.class).append(".HTTP_CREATED: return \"Created\";").newLine(); //$NON-NLS-1$ //$NON-NLS-2$
		it.append("case ").append(HttpURLConnection.class).append(".HTTP_ACCEPTED: return \"Accepted\";").newLine(); //$NON-NLS-1$ //$NON-NLS-2$
		it.append("case ").append(HttpURLConnection.class).append(".HTTP_NOT_AUTHORITATIVE: return \"Non-Authoritative Information\";").newLine(); //$NON-NLS-1$ //$NON-NLS-2$
		it.append("case ").append(HttpURLConnection.class).append(".HTTP_NO_CONTENT: return \"No Content\";").newLine(); //$NON-NLS-1$ //$NON-NLS-2$
		it.append("case ").append(HttpURLConnection.class).append(".HTTP_RESET: return \"Reset Content\";").newLine(); //$NON-NLS-1$ //$NON-NLS-2$
		it.append("case ").append(HttpURLConnection.class).append(".HTTP_PARTIAL: return \"Partial Content\";").newLine(); //$NON-NLS-1$ //$NON-NLS-2$
		it.append("case ").append(HttpURLConnection.class).append(".HTTP_MULT_CHOICE: return \"Multiple Choices\";").newLine(); //$NON-NLS-1$ //$NON-NLS-2$
		it.append("case ").append(HttpURLConnection.class).append(".HTTP_MOVED_PERM: return \"Moved Permanently\";").newLine(); //$NON-NLS-1$ //$NON-NLS-2$
		it.append("case ").append(HttpURLConnection.class).append(".HTTP_MOVED_TEMP: return \"Temporary Redirect\";").newLine(); //$NON-NLS-1$ //$NON-NLS-2$
		it.append("case ").append(HttpURLConnection.class).append(".HTTP_SEE_OTHER: return \"See Other\";").newLine(); //$NON-NLS-1$ //$NON-NLS-2$
		it.append("case ").append(HttpURLConnection.class).append(".HTTP_NOT_MODIFIED: return \"Not Modified\";").newLine(); //$NON-NLS-1$ //$NON-NLS-2$
		it.append("case ").append(HttpURLConnection.class).append(".HTTP_USE_PROXY: return \"Use Proxy\";").newLine(); //$NON-NLS-1$ //$NON-NLS-2$
		it.append("case ").append(HttpURLConnection.class).append(".HTTP_BAD_REQUEST: return \"Bad Request\";").newLine(); //$NON-NLS-1$ //$NON-NLS-2$
		it.append("case ").append(HttpURLConnection.class).append(".HTTP_UNAUTHORIZED: return \"Unauthorized\";").newLine(); //$NON-NLS-1$ //$NON-NLS-2$
		it.append("case ").append(HttpURLConnection.class).append(".HTTP_PAYMENT_REQUIRED: return \"Payment Required\";").newLine(); //$NON-NLS-1$ //$NON-NLS-2$
		it.append("case ").append(HttpURLConnection.class).append(".HTTP_FORBIDDEN: return \"Forbidden\";").newLine(); //$NON-NLS-1$ //$NON-NLS-2$
		it.append("case ").append(HttpURLConnection.class).append(".HTTP_NOT_FOUND: return \"Not Found\";").newLine(); //$NON-NLS-1$ //$NON-NLS-2$
		it.append("case ").append(HttpURLConnection.class).append(".HTTP_NOT_ACCEPTABLE: return \"Not Acceptable\";").newLine(); //$NON-NLS-1$ //$NON-NLS-2$
		it.append("case ").append(HttpURLConnection.class).append(".HTTP_PROXY_AUTH: return \"Proxy Authentication Required\";").newLine(); //$NON-NLS-1$ //$NON-NLS-2$
		it.append("case ").append(HttpURLConnection.class).append(".HTTP_CLIENT_TIMEOUT: return \"Request Time-Out\";").newLine(); //$NON-NLS-1$ //$NON-NLS-2$
		it.append("case ").append(HttpURLConnection.class).append(".HTTP_CONFLICT: return \"Conflict\";").newLine(); //$NON-NLS-1$ //$NON-NLS-2$
		it.append("case ").append(HttpURLConnection.class).append(".HTTP_GONE: return \"Gone\";").newLine(); //$NON-NLS-1$ //$NON-NLS-2$
		it.append("case ").append(HttpURLConnection.class).append(".HTTP_LENGTH_REQUIRED: return \"Length Required\";").newLine(); //$NON-NLS-1$ //$NON-NLS-2$
		it.append("case ").append(HttpURLConnection.class).append(".HTTP_PRECON_FAILED: return \"Precondition Failed\";").newLine(); //$NON-NLS-1$ //$NON-NLS-2$
