ShaclValidatorFluentApiTest.java

/*******************************************************************************
 * Copyright (c) 2025 Eclipse RDF4J contributors.
 *
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Distribution License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * SPDX-License-Identifier: BSD-3-Clause
 *******************************************************************************/
// Some portions generated by Codex

package org.eclipse.rdf4j.sail.shacl;

import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.InputStream;
import java.io.StringReader;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.CountDownLatch;

import org.eclipse.rdf4j.model.IRI;
import org.eclipse.rdf4j.model.Resource;
import org.eclipse.rdf4j.model.ValueFactory;
import org.eclipse.rdf4j.model.impl.SimpleValueFactory;
import org.eclipse.rdf4j.model.vocabulary.RDF;
import org.eclipse.rdf4j.model.vocabulary.RDF4J;
import org.eclipse.rdf4j.model.vocabulary.RDFS;
import org.eclipse.rdf4j.model.vocabulary.SHACL;
import org.eclipse.rdf4j.repository.sail.SailRepository;
import org.eclipse.rdf4j.repository.sail.SailRepositoryConnection;
import org.eclipse.rdf4j.rio.RDFFormat;
import org.eclipse.rdf4j.sail.Sail;
import org.eclipse.rdf4j.sail.SailConnection;
import org.eclipse.rdf4j.sail.SailException;
import org.eclipse.rdf4j.sail.helpers.AbstractSail;
import org.eclipse.rdf4j.sail.memory.MemoryStore;
import org.eclipse.rdf4j.sail.shacl.results.ValidationReport;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Timeout;
import org.junit.jupiter.api.io.TempDir;

class ShaclValidatorFluentApiTest {

	@Test
	void builderExposesShaclSailConfigurationMethods() {
		assertAll(
				() -> assertHasMethod(ShaclValidator.Builder.class, "setGlobalLogValidationExecution", boolean.class),
				() -> assertHasMethod(ShaclValidator.Builder.class, "setLogValidationViolations", boolean.class),
				() -> assertHasMethod(ShaclValidator.Builder.class, "setParallelValidation", boolean.class),
				() -> assertHasMethod(ShaclValidator.Builder.class, "setCacheSelectNodes", boolean.class),
				() -> assertHasMethod(ShaclValidator.Builder.class, "setRdfsSubClassReasoning", boolean.class),
				() -> assertHasMethod(ShaclValidator.Builder.class, "disableValidation"),
				() -> assertHasMethod(ShaclValidator.Builder.class, "enableValidation"),
				() -> assertHasMethod(ShaclValidator.Builder.class, "setLogValidationPlans", boolean.class),
				() -> assertHasMethod(ShaclValidator.Builder.class, "setPerformanceLogging", boolean.class),
				() -> assertHasMethod(ShaclValidator.Builder.class, "setSerializableValidation", boolean.class),
				() -> assertHasMethod(ShaclValidator.Builder.class, "setEclipseRdf4jShaclExtensions", boolean.class),
				() -> assertHasMethod(ShaclValidator.Builder.class, "setDashDataShapes", boolean.class),
				() -> assertHasMethod(ShaclValidator.Builder.class, "setValidationResultsLimitPerConstraint",
						long.class),
				() -> assertHasMethod(ShaclValidator.Builder.class, "setValidationResultsLimitTotal", long.class),
				() -> assertHasMethod(ShaclValidator.Builder.class, "setTransactionalValidationLimit", long.class),
				() -> assertHasMethod(ShaclValidator.Builder.class, "setValidationTimeoutMillis", long.class),
				() -> assertHasMethod(ShaclValidator.Builder.class, "setShapesGraphs", Set.class),
				() -> assertHasMethod(ShaclValidator.Builder.class, "withShapes", File.class, String.class,
						RDFFormat.class),
				() -> assertHasMethod(ShaclValidator.Builder.class, "withShapes", Path.class, String.class,
						RDFFormat.class),
				() -> assertHasMethod(ShaclValidator.Builder.class, "withShapes", URL.class, String.class,
						RDFFormat.class),
				() -> assertHasMethod(ShaclValidator.Builder.class, "withShapes", InputStream.class, String.class,
						RDFFormat.class),
				() -> assertHasMethod(ShaclValidator.Builder.class, "withShapes", String.class, String.class,
						RDFFormat.class),
				() -> assertHasMethod(ShaclValidator.Builder.class, "withShapes", File.class, String.class),
				() -> assertHasMethod(ShaclValidator.Builder.class, "withShapes", Path.class, String.class),
				() -> assertHasMethod(ShaclValidator.Builder.class, "withShapes", URL.class, String.class),
				() -> assertHasMethod(ShaclValidator.Builder.class, "withShapes", InputStream.class, String.class),
				() -> assertHasMethod(ShaclValidator.Builder.class, "withShapes", String.class, String.class),
				() -> assertHasMethod(ShaclValidator.Builder.class, "withShapes", File.class, RDFFormat.class),
				() -> assertHasMethod(ShaclValidator.Builder.class, "withShapes", Path.class, RDFFormat.class),
				() -> assertHasMethod(ShaclValidator.Builder.class, "withShapes", URL.class, RDFFormat.class),
				() -> assertHasMethod(ShaclValidator.Builder.class, "withShapes", InputStream.class, RDFFormat.class),
				() -> assertHasMethod(ShaclValidator.Builder.class, "withShapes", String.class, RDFFormat.class),
				() -> assertHasMethod(ShaclValidator.Builder.class, "withShapes", File.class),
				() -> assertHasMethod(ShaclValidator.Builder.class, "withShapes", Path.class),
				() -> assertHasMethod(ShaclValidator.Builder.class, "withShapes", URL.class));
	}

	@Test
	void validatorExposesShapeSourceValidationOverloads() {
		assertAll(
				() -> assertHasMethod(ShaclValidator.Validator.class, "validate", Sail.class, File.class,
						String.class, RDFFormat.class),
				() -> assertHasMethod(ShaclValidator.Validator.class, "validate", Sail.class, Path.class,
						String.class, RDFFormat.class),
				() -> assertHasMethod(ShaclValidator.Validator.class, "validate", Sail.class, URL.class,
						String.class, RDFFormat.class),
				() -> assertHasMethod(ShaclValidator.Validator.class, "validate", Sail.class, InputStream.class,
						String.class, RDFFormat.class),
				() -> assertHasMethod(ShaclValidator.Validator.class, "validate", Sail.class, String.class,
						String.class, RDFFormat.class),
				() -> assertHasMethod(ShaclValidator.Validator.class, "validate", Sail.class, File.class,
						RDFFormat.class),
				() -> assertHasMethod(ShaclValidator.Validator.class, "validate", Sail.class, Path.class,
						RDFFormat.class),
				() -> assertHasMethod(ShaclValidator.Validator.class, "validate", Sail.class, URL.class,
						RDFFormat.class),
				() -> assertHasMethod(ShaclValidator.Validator.class, "validate", Sail.class, InputStream.class,
						RDFFormat.class),
				() -> assertHasMethod(ShaclValidator.Validator.class, "validate", Sail.class, String.class,
						RDFFormat.class),
				() -> assertHasMethod(ShaclValidator.Validator.class, "validate", Sail.class, File.class),
				() -> assertHasMethod(ShaclValidator.Validator.class, "validate", Sail.class, Path.class),
				() -> assertHasMethod(ShaclValidator.Validator.class, "validate", Sail.class, URL.class));
	}

	@Test
	void validatorExposesDataSourceValidationOverloads() {
		assertAll(
				() -> assertHasMethod(ShaclValidator.Validator.class, "validate", File.class, String.class,
						RDFFormat.class, Sail.class),
				() -> assertHasMethod(ShaclValidator.Validator.class, "validate", Path.class, String.class,
						RDFFormat.class, Sail.class),
				() -> assertHasMethod(ShaclValidator.Validator.class, "validate", URL.class, String.class,
						RDFFormat.class, Sail.class),
				() -> assertHasMethod(ShaclValidator.Validator.class, "validate", InputStream.class, String.class,
						RDFFormat.class, Sail.class),
				() -> assertHasMethod(ShaclValidator.Validator.class, "validate", String.class, String.class,
						RDFFormat.class, Sail.class),
				() -> assertHasMethod(ShaclValidator.Validator.class, "validate", File.class, String.class,
						Sail.class),
				() -> assertHasMethod(ShaclValidator.Validator.class, "validate", Path.class, String.class,
						Sail.class),
				() -> assertHasMethod(ShaclValidator.Validator.class, "validate", URL.class, String.class,
						Sail.class),
				() -> assertHasMethod(ShaclValidator.Validator.class, "validate", InputStream.class, String.class,
						Sail.class),
				() -> assertHasMethod(ShaclValidator.Validator.class, "validate", String.class, String.class,
						Sail.class),
				() -> assertHasMethod(ShaclValidator.Validator.class, "validate", File.class, RDFFormat.class,
						Sail.class),
				() -> assertHasMethod(ShaclValidator.Validator.class, "validate", Path.class, RDFFormat.class,
						Sail.class),
				() -> assertHasMethod(ShaclValidator.Validator.class, "validate", URL.class, RDFFormat.class,
						Sail.class),
				() -> assertHasMethod(ShaclValidator.Validator.class, "validate", InputStream.class, RDFFormat.class,
						Sail.class),
				() -> assertHasMethod(ShaclValidator.Validator.class, "validate", String.class, RDFFormat.class,
						Sail.class),
				() -> assertHasMethod(ShaclValidator.Validator.class, "validate", File.class, Sail.class),
				() -> assertHasMethod(ShaclValidator.Validator.class, "validate", Path.class, Sail.class),
				() -> assertHasMethod(ShaclValidator.Validator.class, "validate", URL.class, Sail.class));
	}

	@Test
	void validatorWithShapesExposesDataSourceValidationOverloads() {
		assertAll(
				() -> assertHasMethod(ShaclValidator.ValidatorWithShapes.class, "validate", File.class, String.class,
						RDFFormat.class),
				() -> assertHasMethod(ShaclValidator.ValidatorWithShapes.class, "validate", Path.class, String.class,
						RDFFormat.class),
				() -> assertHasMethod(ShaclValidator.ValidatorWithShapes.class, "validate", URL.class, String.class,
						RDFFormat.class),
				() -> assertHasMethod(ShaclValidator.ValidatorWithShapes.class, "validate", InputStream.class,
						String.class, RDFFormat.class),
				() -> assertHasMethod(ShaclValidator.ValidatorWithShapes.class, "validate", String.class,
						String.class, RDFFormat.class),
				() -> assertHasMethod(ShaclValidator.ValidatorWithShapes.class, "validate", File.class, String.class),
				() -> assertHasMethod(ShaclValidator.ValidatorWithShapes.class, "validate", Path.class, String.class),
				() -> assertHasMethod(ShaclValidator.ValidatorWithShapes.class, "validate", URL.class, String.class),
				() -> assertHasMethod(ShaclValidator.ValidatorWithShapes.class, "validate", InputStream.class,
						String.class),
				() -> assertHasMethod(ShaclValidator.ValidatorWithShapes.class, "validate", String.class,
						String.class),
				() -> assertHasMethod(ShaclValidator.ValidatorWithShapes.class, "validate", File.class,
						RDFFormat.class),
				() -> assertHasMethod(ShaclValidator.ValidatorWithShapes.class, "validate", Path.class,
						RDFFormat.class),
				() -> assertHasMethod(ShaclValidator.ValidatorWithShapes.class, "validate", URL.class,
						RDFFormat.class),
				() -> assertHasMethod(ShaclValidator.ValidatorWithShapes.class, "validate", InputStream.class,
						RDFFormat.class),
				() -> assertHasMethod(ShaclValidator.ValidatorWithShapes.class, "validate", String.class,
						RDFFormat.class),
				() -> assertHasMethod(ShaclValidator.ValidatorWithShapes.class, "validate", File.class),
				() -> assertHasMethod(ShaclValidator.ValidatorWithShapes.class, "validate", Path.class),
				() -> assertHasMethod(ShaclValidator.ValidatorWithShapes.class, "validate", URL.class));
	}

	@Test
	void builderWithShapesFromFileLoadsShapesUsingBaseUriAndFormat(@TempDir Path tempDir) throws Exception {
		String baseUri = "http://example.com/ns";
		String shapesTtl = relativeShapesTtl();
		Path shapesPath = writeShapesFile(tempDir, shapesTtl);

		SailRepository dataRepo = createDataRepoForBaseUri(baseUri);
		try {
			ValidationReport report = validateWithShapes(shapesPath.toFile(), baseUri, RDFFormat.TURTLE, dataRepo);
			assertFalse(report.conforms(), "expected validation to run when loading shapes from File");
		} finally {
			dataRepo.shutDown();
		}
	}

	@Test
	void builderWithShapesFromPathLoadsShapesUsingBaseUriAndFormat(@TempDir Path tempDir) throws Exception {
		String baseUri = "http://example.com/ns";
		String shapesTtl = relativeShapesTtl();
		Path shapesPath = writeShapesFile(tempDir, shapesTtl);

		SailRepository dataRepo = createDataRepoForBaseUri(baseUri);
		try {
			ValidationReport report = validateWithShapes(shapesPath, baseUri, RDFFormat.TURTLE, dataRepo);
			assertFalse(report.conforms(), "expected validation to run when loading shapes from Path");
		} finally {
			dataRepo.shutDown();
		}
	}

	@Test
	void builderWithShapesFromUrlLoadsShapesUsingBaseUriAndFormat(@TempDir Path tempDir) throws Exception {
		String baseUri = "http://example.com/ns";
		String shapesTtl = relativeShapesTtl();
		Path shapesPath = writeShapesFile(tempDir, shapesTtl);

		SailRepository dataRepo = createDataRepoForBaseUri(baseUri);
		try {
			ValidationReport report = validateWithShapes(shapesPath.toUri().toURL(), baseUri, RDFFormat.TURTLE,
					dataRepo);
			assertFalse(report.conforms(), "expected validation to run when loading shapes from URL");
		} finally {
			dataRepo.shutDown();
		}
	}

	@Test
	void builderWithShapesFromInputStreamLoadsShapesUsingBaseUriAndFormat() throws Exception {
		String baseUri = "http://example.com/ns";
		String shapesTtl = relativeShapesTtl();

		SailRepository dataRepo = createDataRepoForBaseUri(baseUri);
		try (InputStream inputStream = new ByteArrayInputStream(shapesTtl.getBytes(StandardCharsets.UTF_8))) {
			ValidationReport report = validateWithShapes(inputStream, baseUri, RDFFormat.TURTLE, dataRepo);
			assertFalse(report.conforms(), "expected validation to run when loading shapes from InputStream");
		} finally {
			dataRepo.shutDown();
		}
	}

	@Test
	void builderWithShapesFromStringLoadsShapesUsingBaseUriAndFormat() throws Exception {
		String baseUri = "http://example.com/ns";
		String shapesTtl = relativeShapesTtl();

		SailRepository dataRepo = createDataRepoForBaseUri(baseUri);
		try {
			ValidationReport report = validateWithShapes(shapesTtl, baseUri, RDFFormat.TURTLE, dataRepo);
			assertFalse(report.conforms(), "expected validation to run when loading shapes from String content");
		} finally {
			dataRepo.shutDown();
		}
	}

	@Test
	void builderWithShapesFromFileAutoDetectsFormat(@TempDir Path tempDir) throws Exception {
		String baseUri = "http://example.com/ns";
		String shapesTtl = relativeShapesTtl();
		Path shapesPath = writeShapesFile(tempDir, shapesTtl);

		SailRepository dataRepo = createDataRepoForBaseUri(baseUri);
		try {
			ValidationReport report = validateWithShapesAuto(shapesPath.toFile(), baseUri, dataRepo);
			assertFalse(report.conforms(), "expected validation to run when auto-detecting format from File");
		} finally {
			dataRepo.shutDown();
		}
	}

	@Test
	void builderWithShapesFromPathAutoDetectsFormat(@TempDir Path tempDir) throws Exception {
		String baseUri = "http://example.com/ns";
		String shapesTtl = relativeShapesTtl();
		Path shapesPath = writeShapesFile(tempDir, shapesTtl);

		SailRepository dataRepo = createDataRepoForBaseUri(baseUri);
		try {
			ValidationReport report = validateWithShapesAuto(shapesPath, baseUri, dataRepo);
			assertFalse(report.conforms(), "expected validation to run when auto-detecting format from Path");
		} finally {
			dataRepo.shutDown();
		}
	}

	@Test
	void builderWithShapesFromUrlAutoDetectsFormat(@TempDir Path tempDir) throws Exception {
		String baseUri = "http://example.com/ns";
		String shapesTtl = relativeShapesTtl();
		Path shapesPath = writeShapesFile(tempDir, shapesTtl);

		SailRepository dataRepo = createDataRepoForBaseUri(baseUri);
		try {
			ValidationReport report = validateWithShapesAuto(shapesPath.toUri().toURL(), baseUri, dataRepo);
			assertFalse(report.conforms(), "expected validation to run when auto-detecting format from URL");
		} finally {
			dataRepo.shutDown();
		}
	}

	@Test
	void builderWithShapesFromInputStreamAutoDetectsFormat() throws Exception {
		String baseUri = "http://example.com/shapes.ttl";
		String shapesTtl = relativeShapesTtl();

		SailRepository dataRepo = createDataRepoForBaseUri(baseUri);
		try (InputStream inputStream = new ByteArrayInputStream(shapesTtl.getBytes(StandardCharsets.UTF_8))) {
			ValidationReport report = validateWithShapesAuto(inputStream, baseUri, dataRepo);
			assertFalse(report.conforms(), "expected validation to run when auto-detecting format from InputStream");
		} finally {
			dataRepo.shutDown();
		}
	}

	@Test
	void builderWithShapesFromStringAutoDetectsFormat() throws Exception {
		String baseUri = "http://example.com/shapes.ttl";
		String shapesTtl = relativeShapesTtl();

		SailRepository dataRepo = createDataRepoForBaseUri(baseUri);
		try {
			ValidationReport report = validateWithShapesAuto(shapesTtl, baseUri, dataRepo);
			assertFalse(report.conforms(), "expected validation to run when auto-detecting format from String");
		} finally {
			dataRepo.shutDown();
		}
	}

	@Test
	void builderWithShapesFromFileWithoutBaseUriLoadsShapes(@TempDir Path tempDir) throws Exception {
		String shapesTtl = absoluteShapesTtl();
		Path shapesPath = writeShapesFile(tempDir, shapesTtl);

		SailRepository dataRepo = createDataRepoForAbsoluteShapes();
		try {
			ValidationReport report = validateWithShapesNoBaseUri(shapesPath.toFile(), RDFFormat.TURTLE, dataRepo);
			assertFalse(report.conforms(), "expected validation to run when loading shapes from File without base URI");
		} finally {
			dataRepo.shutDown();
		}
	}

	@Test
	void builderWithShapesFromPathWithoutBaseUriLoadsShapes(@TempDir Path tempDir) throws Exception {
		String shapesTtl = absoluteShapesTtl();
		Path shapesPath = writeShapesFile(tempDir, shapesTtl);

		SailRepository dataRepo = createDataRepoForAbsoluteShapes();
		try {
			ValidationReport report = validateWithShapesNoBaseUri(shapesPath, RDFFormat.TURTLE, dataRepo);
			assertFalse(report.conforms(), "expected validation to run when loading shapes from Path without base URI");
		} finally {
			dataRepo.shutDown();
		}
	}

	@Test
	void builderWithShapesFromUrlWithoutBaseUriLoadsShapes(@TempDir Path tempDir) throws Exception {
		String shapesTtl = absoluteShapesTtl();
		Path shapesPath = writeShapesFile(tempDir, shapesTtl);

		SailRepository dataRepo = createDataRepoForAbsoluteShapes();
		try {
			ValidationReport report = validateWithShapesNoBaseUri(shapesPath.toUri().toURL(), RDFFormat.TURTLE,
					dataRepo);
			assertFalse(report.conforms(), "expected validation to run when loading shapes from URL without base URI");
		} finally {
			dataRepo.shutDown();
		}
	}

	@Test
	void builderWithShapesFromInputStreamWithoutBaseUriLoadsShapes() throws Exception {
		String shapesTtl = absoluteShapesTtl();

		SailRepository dataRepo = createDataRepoForAbsoluteShapes();
		try (InputStream inputStream = new ByteArrayInputStream(shapesTtl.getBytes(StandardCharsets.UTF_8))) {
			ValidationReport report = validateWithShapesNoBaseUri(inputStream, RDFFormat.TURTLE, dataRepo);
			assertFalse(report.conforms(),
					"expected validation to run when loading shapes from InputStream without base URI");
		} finally {
			dataRepo.shutDown();
		}
	}

	@Test
	void builderWithShapesFromStringWithoutBaseUriLoadsShapes() throws Exception {
		String shapesTtl = absoluteShapesTtl();

		SailRepository dataRepo = createDataRepoForAbsoluteShapes();
		try {
			ValidationReport report = validateWithShapesNoBaseUri(shapesTtl, RDFFormat.TURTLE, dataRepo);
			assertFalse(report.conforms(),
					"expected validation to run when loading shapes from String without base URI");
		} finally {
			dataRepo.shutDown();
		}
	}

	@Test
	void builderWithShapesFromFileAutoDetectsFormatWithoutBaseUri(@TempDir Path tempDir) throws Exception {
		String shapesTtl = absoluteShapesTtl();
		Path shapesPath = writeShapesFile(tempDir, shapesTtl);

		SailRepository dataRepo = createDataRepoForAbsoluteShapes();
		try {
			ValidationReport report = validateWithShapesAutoNoBaseUri(shapesPath.toFile(), dataRepo);
			assertFalse(report.conforms(),
					"expected validation to run when auto-detecting format from File without base URI");
		} finally {
			dataRepo.shutDown();
		}
	}

	@Test
	void builderWithShapesFromPathAutoDetectsFormatWithoutBaseUri(@TempDir Path tempDir) throws Exception {
		String shapesTtl = absoluteShapesTtl();
		Path shapesPath = writeShapesFile(tempDir, shapesTtl);

		SailRepository dataRepo = createDataRepoForAbsoluteShapes();
		try {
			ValidationReport report = validateWithShapesAutoNoBaseUri(shapesPath, dataRepo);
			assertFalse(report.conforms(),
					"expected validation to run when auto-detecting format from Path without base URI");
		} finally {
			dataRepo.shutDown();
		}
	}

	@Test
	void builderWithShapesFromUrlAutoDetectsFormatWithoutBaseUri(@TempDir Path tempDir) throws Exception {
		String shapesTtl = absoluteShapesTtl();
		Path shapesPath = writeShapesFile(tempDir, shapesTtl);

		SailRepository dataRepo = createDataRepoForAbsoluteShapes();
		try {
			ValidationReport report = validateWithShapesAutoNoBaseUri(shapesPath.toUri().toURL(), dataRepo);
			assertFalse(report.conforms(),
					"expected validation to run when auto-detecting format from URL without base URI");
		} finally {
			dataRepo.shutDown();
		}
	}

	@Test
	void validatorLoadsDataFromFileUsingBaseUriAndFormat(@TempDir Path tempDir) throws Exception {
		String baseUri = "http://example.com/ns";
		Path dataPath = writeDataFile(tempDir, relativeDataTtl());

		SailRepository shapesRepo = createShapesRepoForBaseUri(baseUri);
		try {
			ValidationReport report = validateWithValidatorData(dataPath.toFile(), baseUri, RDFFormat.TURTLE,
					shapesRepo.getSail());
			assertFalse(report.conforms(), "expected validator to load data from File");
		} finally {
			shapesRepo.shutDown();
		}
	}

	@Test
	void validatorAutoDetectsDataFormatWithBaseUri(@TempDir Path tempDir) throws Exception {
		String baseUri = "http://example.com/ns/data.ttl";
		Path dataPath = writeDataFile(tempDir, relativeDataTtl());

		SailRepository shapesRepo = createShapesRepoForBaseUri(baseUri);
		try {
			ValidationReport report = validateWithValidatorDataAuto(dataPath.toUri().toURL(), baseUri,
					shapesRepo.getSail());
			assertFalse(report.conforms(), "expected validator to auto-detect data format with base URI");
		} finally {
			shapesRepo.shutDown();
		}
	}

	@Test
	void validatorLoadsDataWithoutBaseUriUsingFormat(@TempDir Path tempDir) throws Exception {
		Path dataPath = writeDataFile(tempDir, absoluteDataTtl());

		SailRepository shapesRepo = createShapesRepoForAbsoluteShapes();
		try {
			ValidationReport report = validateWithValidatorDataNoBaseUri(dataPath, RDFFormat.TURTLE,
					shapesRepo.getSail());
			assertFalse(report.conforms(), "expected validator to load data without base URI");
		} finally {
			shapesRepo.shutDown();
		}
	}

	@Test
	void validatorAutoDetectsDataFormatWithoutBaseUri(@TempDir Path tempDir) throws Exception {
		Path dataPath = writeDataFile(tempDir, absoluteDataTtl());

		SailRepository shapesRepo = createShapesRepoForAbsoluteShapes();
		try {
			ValidationReport report = validateWithValidatorDataAutoNoBaseUri(dataPath.toFile(),
					shapesRepo.getSail());
			assertFalse(report.conforms(), "expected validator to auto-detect data format without base URI");
		} finally {
			shapesRepo.shutDown();
		}
	}

	@Test
	void validatorWithShapesLoadsDataFromFileUsingBaseUriAndFormat(@TempDir Path tempDir) throws Exception {
		String baseUri = "http://example.com/ns";
		String shapesTtl = relativeShapesTtl();
		String dataTtl = relativeDataTtl();
		Path dataPath = writeDataFile(tempDir, dataTtl);

		ValidationReport report = validateWithData(dataPath.toFile(), baseUri, RDFFormat.TURTLE, shapesTtl);
		assertFalse(report.conforms(), "expected validation to run when loading data from File");
	}

	@Test
	void validatorWithShapesLoadsDataFromPathUsingBaseUriAndFormat(@TempDir Path tempDir) throws Exception {
		String baseUri = "http://example.com/ns";
		String shapesTtl = relativeShapesTtl();
		String dataTtl = relativeDataTtl();
		Path dataPath = writeDataFile(tempDir, dataTtl);

		ValidationReport report = validateWithData(dataPath, baseUri, RDFFormat.TURTLE, shapesTtl);
		assertFalse(report.conforms(), "expected validation to run when loading data from Path");
	}

	@Test
	void validatorWithShapesLoadsDataFromUrlUsingBaseUriAndFormat(@TempDir Path tempDir) throws Exception {
		String baseUri = "http://example.com/ns";
		String shapesTtl = relativeShapesTtl();
		String dataTtl = relativeDataTtl();
		Path dataPath = writeDataFile(tempDir, dataTtl);

		ValidationReport report = validateWithData(dataPath.toUri().toURL(), baseUri, RDFFormat.TURTLE, shapesTtl);
		assertFalse(report.conforms(), "expected validation to run when loading data from URL");
	}

	@Test
	void validatorWithShapesLoadsDataFromInputStreamUsingBaseUriAndFormat() throws Exception {
		String baseUri = "http://example.com/ns";
		String shapesTtl = relativeShapesTtl();
		String dataTtl = relativeDataTtl();

		try (InputStream inputStream = new ByteArrayInputStream(dataTtl.getBytes(StandardCharsets.UTF_8))) {
			ValidationReport report = validateWithData(inputStream, baseUri, RDFFormat.TURTLE, shapesTtl);
			assertFalse(report.conforms(), "expected validation to run when loading data from InputStream");
		}
	}

	@Test
	void validatorWithShapesLoadsDataFromStringUsingBaseUriAndFormat() throws Exception {
		String baseUri = "http://example.com/ns";
		String shapesTtl = relativeShapesTtl();
		String dataTtl = relativeDataTtl();

		ValidationReport report = validateWithData(dataTtl, baseUri, RDFFormat.TURTLE, shapesTtl);
		assertFalse(report.conforms(), "expected validation to run when loading data from String");
	}

	@Test
	void validatorWithShapesAutoDetectsDataFormatFromFileWithBaseUri(@TempDir Path tempDir) throws Exception {
		String baseUri = "http://example.com/ns";
		String shapesTtl = relativeShapesTtl();
		String dataTtl = relativeDataTtl();
		Path dataPath = writeDataFile(tempDir, dataTtl);

		ValidationReport report = validateWithDataAuto(dataPath.toFile(), baseUri, shapesTtl);
		assertFalse(report.conforms(), "expected validation to auto-detect data format from File");
	}

	@Test
	void validatorWithShapesAutoDetectsDataFormatFromPathWithBaseUri(@TempDir Path tempDir) throws Exception {
		String baseUri = "http://example.com/ns";
		String shapesTtl = relativeShapesTtl();
		String dataTtl = relativeDataTtl();
		Path dataPath = writeDataFile(tempDir, dataTtl);

		ValidationReport report = validateWithDataAuto(dataPath, baseUri, shapesTtl);
		assertFalse(report.conforms(), "expected validation to auto-detect data format from Path");
	}

	@Test
	void validatorWithShapesAutoDetectsDataFormatFromUrlWithBaseUri(@TempDir Path tempDir) throws Exception {
		String baseUri = "http://example.com/ns";
		String shapesTtl = relativeShapesTtl();
		String dataTtl = relativeDataTtl();
		Path dataPath = writeDataFile(tempDir, dataTtl);

		ValidationReport report = validateWithDataAuto(dataPath.toUri().toURL(), baseUri, shapesTtl);
		assertFalse(report.conforms(), "expected validation to auto-detect data format from URL");
	}

	@Test
	void validatorWithShapesAutoDetectsDataFormatFromInputStreamWithBaseUri() throws Exception {
		String baseUri = "http://example.com/data.ttl";
		String shapesTtl = relativeShapesTtl();
		String dataTtl = relativeDataTtl();

		try (InputStream inputStream = new ByteArrayInputStream(dataTtl.getBytes(StandardCharsets.UTF_8))) {
			ValidationReport report = validateWithDataAuto(inputStream, baseUri, shapesTtl);
			assertFalse(report.conforms(), "expected validation to auto-detect data format from InputStream");
		}
	}

	@Test
	void validatorWithShapesAutoDetectsDataFormatFromStringWithBaseUri() throws Exception {
		String baseUri = "http://example.com/data.ttl";
		String shapesTtl = relativeShapesTtl();
		String dataTtl = relativeDataTtl();

		ValidationReport report = validateWithDataAuto(dataTtl, baseUri, shapesTtl);
		assertFalse(report.conforms(), "expected validation to auto-detect data format from String");
	}

	@Test
	void validatorWithShapesLoadsDataFromFileWithoutBaseUriUsingFormat(@TempDir Path tempDir) throws Exception {
		String shapesTtl = absoluteShapesTtl();
		String dataTtl = absoluteDataTtl();
		Path dataPath = writeDataFile(tempDir, dataTtl);

		ValidationReport report = validateWithDataNoBaseUri(dataPath.toFile(), RDFFormat.TURTLE, shapesTtl);
		assertFalse(report.conforms(), "expected validation to run when loading data from File without base URI");
	}

	@Test
	void validatorWithShapesLoadsDataFromPathWithoutBaseUriUsingFormat(@TempDir Path tempDir) throws Exception {
		String shapesTtl = absoluteShapesTtl();
		String dataTtl = absoluteDataTtl();
		Path dataPath = writeDataFile(tempDir, dataTtl);

		ValidationReport report = validateWithDataNoBaseUri(dataPath, RDFFormat.TURTLE, shapesTtl);
		assertFalse(report.conforms(), "expected validation to run when loading data from Path without base URI");
	}

	@Test
	void validatorWithShapesLoadsDataFromUrlWithoutBaseUriUsingFormat(@TempDir Path tempDir) throws Exception {
		String shapesTtl = absoluteShapesTtl();
		String dataTtl = absoluteDataTtl();
		Path dataPath = writeDataFile(tempDir, dataTtl);

		ValidationReport report = validateWithDataNoBaseUri(dataPath.toUri().toURL(), RDFFormat.TURTLE, shapesTtl);
		assertFalse(report.conforms(), "expected validation to run when loading data from URL without base URI");
	}

	@Test
	void validatorWithShapesLoadsDataFromInputStreamWithoutBaseUriUsingFormat() throws Exception {
		String shapesTtl = absoluteShapesTtl();
		String dataTtl = absoluteDataTtl();

		try (InputStream inputStream = new ByteArrayInputStream(dataTtl.getBytes(StandardCharsets.UTF_8))) {
			ValidationReport report = validateWithDataNoBaseUri(inputStream, RDFFormat.TURTLE, shapesTtl);
			assertFalse(report.conforms(),
					"expected validation to run when loading data from InputStream without base URI");
		}
	}

	@Test
	void validatorWithShapesLoadsDataFromStringWithoutBaseUriUsingFormat() throws Exception {
		String shapesTtl = absoluteShapesTtl();
		String dataTtl = absoluteDataTtl();

		ValidationReport report = validateWithDataNoBaseUri(dataTtl, RDFFormat.TURTLE, shapesTtl);
		assertFalse(report.conforms(), "expected validation to run when loading data from String without base URI");
	}

	@Test
	void validatorWithShapesAutoDetectsDataFormatFromFileWithoutBaseUri(@TempDir Path tempDir) throws Exception {
		String shapesTtl = absoluteShapesTtl();
		String dataTtl = absoluteDataTtl();
		Path dataPath = writeDataFile(tempDir, dataTtl);

		ValidationReport report = validateWithDataAutoNoBaseUri(dataPath.toFile(), shapesTtl);
		assertFalse(report.conforms(), "expected validation to auto-detect data format from File without base URI");
	}

	@Test
	void validatorWithShapesAutoDetectsDataFormatFromPathWithoutBaseUri(@TempDir Path tempDir) throws Exception {
		String shapesTtl = absoluteShapesTtl();
		String dataTtl = absoluteDataTtl();
		Path dataPath = writeDataFile(tempDir, dataTtl);

		ValidationReport report = validateWithDataAutoNoBaseUri(dataPath, shapesTtl);
		assertFalse(report.conforms(), "expected validation to auto-detect data format from Path without base URI");
	}

	@Test
	void validatorWithShapesAutoDetectsDataFormatFromUrlWithoutBaseUri(@TempDir Path tempDir) throws Exception {
		String shapesTtl = absoluteShapesTtl();
		String dataTtl = absoluteDataTtl();
		Path dataPath = writeDataFile(tempDir, dataTtl);

		ValidationReport report = validateWithDataAutoNoBaseUri(dataPath.toUri().toURL(), shapesTtl);
		assertFalse(report.conforms(), "expected validation to auto-detect data format from URL without base URI");
	}

	@Test
	void validatorUsesDefensiveCopyOfShapesGraphsSet() throws Exception {

		SailRepository shapesRepo = new SailRepository(new MemoryStore());
		IRI graph1 = iri("http://example.com/graph1");
		IRI graph2 = iri("http://example.com/graph2");
		addShape(shapesRepo, graph1, "http://example.com/ns#p1");
		addShape(shapesRepo, graph2, "http://example.com/ns#p2");

		SailRepository dataRepo = new SailRepository(new MemoryStore());
		addData(dataRepo, "http://example.com/ns#a", "http://example.com/ns#p1");

		Set<IRI> shapesGraphs = new HashSet<>();
		shapesGraphs.add(graph1);

		ShaclValidator.Builder builder = ShaclValidator.builder();
		invoke(builder, "setShapesGraphs", shapesGraphs);
		ShaclValidator.Validator validator = builder.build();

		shapesGraphs.clear();
		shapesGraphs.add(graph2);

		ValidationReport report = validator.validate(dataRepo.getSail(), shapesRepo.getSail());
		assertTrue(report.conforms(), "validator should not be affected by mutations to the original Set");
	}

	@Test
	void setShapesGraphsTreatsRdf4jNilAsDefaultGraphForMappings() throws Exception {
		SailRepository shapesRepo = new SailRepository(new MemoryStore());
		IRI shapesGraph = iri("http://example.com/ns#shapesGraph");

		String shapesTtl = "@prefix sh: <http://www.w3.org/ns/shacl#> .\n" +
				"@prefix ex: <http://example.com/ns#> .\n" +
				"\n" +
				"ex:PersonShape a sh:NodeShape ;\n" +
				"  sh:targetClass ex:Person ;\n" +
				"  sh:property [\n" +
				"    sh:path ex:required ;\n" +
				"    sh:minCount 1 ;\n" +
				"  ] .\n";

		try (SailRepositoryConnection conn = shapesRepo.getConnection()) {
			conn.add(RDF4J.NIL, SHACL.SHAPES_GRAPH, shapesGraph);
			conn.add(new StringReader(shapesTtl), "", RDFFormat.TURTLE, shapesGraph);
		}

		SailRepository dataRepo = new SailRepository(new MemoryStore());
		try (SailRepositoryConnection conn = dataRepo.getConnection()) {
			conn.add(iri("http://example.com/ns#alice"), RDF.TYPE, iri("http://example.com/ns#Person"));
		}

		ShaclValidator.Builder builder = ShaclValidator.builder();
		invoke(builder, "setShapesGraphs", Set.of(RDF4J.NIL));
		ValidationReport report = builder.build().validate(dataRepo.getSail(), shapesRepo.getSail());
		assertFalse(report.conforms(), "expected default-graph mapping statements to be honored via rdf4j:nil");
	}

	@Test
	void setShapesGraphToNullShouldDiscoverAllShapes() throws Exception {

		IRI[] contexts = { null, RDF4J.SHACL_SHAPE_GRAPH, RDF4J.NIL, iri("http://example.com/otherGraph") };

		for (IRI context : contexts) {
			SailRepository shapesRepo = new SailRepository(new MemoryStore());

			String shapesTtl = "@prefix sh: <http://www.w3.org/ns/shacl#> .\n" +
					"@prefix ex: <http://example.com/ns#> .\n" +
					"\n" +
					"ex:PersonShape a sh:NodeShape ;\n" +
					"  sh:targetClass ex:Person ;\n" +
					"  sh:property [\n" +
					"    sh:path ex:required ;\n" +
					"    sh:minCount 1 ;\n" +
					"  ] .\n";

			try (SailRepositoryConnection conn = shapesRepo.getConnection()) {
				if (context == null) {
					conn.add(new StringReader(shapesTtl), "", RDFFormat.TURTLE);
				} else {
					conn.add(new StringReader(shapesTtl), "", RDFFormat.TURTLE, context);
				}
			}

			SailRepository dataRepo = new SailRepository(new MemoryStore());
			try (SailRepositoryConnection conn = dataRepo.getConnection()) {
				conn.add(iri("http://example.com/ns#alice"), RDF.TYPE, iri("http://example.com/ns#Person"));
			}

			ShaclValidator.Builder builder = ShaclValidator.builder();
			builder.setShapesGraphs(null);
			ValidationReport report = builder.build().validate(dataRepo.getSail(), shapesRepo.getSail());
			assertFalse(report.conforms(), "expected all shapes to be discovered when null is provided");
			shapesRepo.shutDown();
		}
	}

	@Test
	void validatorUsesDefensiveCopyOfShapeContextsArray() throws Exception {
		SailRepository shapesRepo = new SailRepository(new MemoryStore());
		IRI graph1 = iri("http://example.com/graph1");
		IRI graph2 = iri("http://example.com/graph2");
		addShape(shapesRepo, graph1, "http://example.com/ns#p1");
		addShape(shapesRepo, graph2, "http://example.com/ns#p2");

		SailRepository dataRepo = new SailRepository(new MemoryStore());
		addData(dataRepo, "http://example.com/ns#a", "http://example.com/ns#p1");

		IRI[] contexts = new IRI[] { graph1 };

		ShaclValidator.Builder builder = ShaclValidator.builder().shapeContexts(contexts);
		ShaclValidator.Validator validator = builder.build();

		contexts[0] = graph2;

		ValidationReport report = validator.validate(dataRepo.getSail(), shapesRepo.getSail());
		assertTrue(report.conforms(), "validator should not be affected by mutations to the original array");
	}

	@Test
	void validatorDefaultsToReadingAllShapeContexts() throws Exception {
		SailRepository shapesRepo = new SailRepository(new MemoryStore());
		addManyViolationsShape(shapesRepo, RDF4J.SHACL_SHAPE_GRAPH);

		SailRepository dataRepo = new SailRepository(new MemoryStore());
		addManyViolationsData(dataRepo, 1);

		ValidationReport report = ShaclValidator.builder()
				.build()
				.validate(dataRepo.getSail(), shapesRepo.getSail());
		assertFalse(report.conforms(), "expected validation to run against all shapes by default");
	}

	@Test
	void builderDefaultsToIncludingDefaultGraphShapes() throws Exception {

		IRI[] contexts = { null, RDF4J.SHACL_SHAPE_GRAPH, RDF4J.NIL, iri("http://example.com/otherGraph") };

		String ttl = "@prefix sh: <http://www.w3.org/ns/shacl#> .\n" +
				"@prefix ex: <http://example.com/ns#> .\n" +
				"\n" +
				"ex:Shape a sh:NodeShape ;\n" +
				"  sh:targetClass ex:Person ;\n" +
				"  sh:property [\n" +
				"    sh:path ex:required ;\n" +
				"    sh:minCount 1 ;\n" +
				"  ] .\n";

		for (IRI context : contexts) {
			SailRepository shapesRepo = new SailRepository(new MemoryStore());

			try (SailRepositoryConnection conn = shapesRepo.getConnection()) {
				if (context == null) {
					conn.add(new StringReader(ttl), "", RDFFormat.TURTLE);
				} else {
					conn.add(new StringReader(ttl), "", RDFFormat.TURTLE, context);
				}
			}

			SailRepository dataRepo = new SailRepository(new MemoryStore());
			addManyViolationsData(dataRepo, 1);

			ValidationReport report = ShaclValidator.builder()
					.build()
					.validate(dataRepo.getSail(), shapesRepo.getSail());
			assertFalse(report.conforms(),
					"builder validation should include all graphs by default, failed for context: " + context);

			shapesRepo.shutDown();
		}

	}

	@Test
	void settingsAreCopiedFromBuilderToBuilderWithShapesToValidatorWithShapes() throws Exception {
		SailRepository shapesRepo = new SailRepository(new MemoryStore());
		IRI graph1 = iri("http://example.com/graph1");
		IRI graph2 = iri("http://example.com/graph2");
		addShape(shapesRepo, graph1, "http://example.com/ns#p1");
		addShape(shapesRepo, graph2, "http://example.com/ns#p2");

		SailRepository dataRepo = new SailRepository(new MemoryStore());
		addData(dataRepo, "http://example.com/ns#a", "http://example.com/ns#p1");

		ShaclValidator.Builder builder = ShaclValidator.builder();
		invoke(builder, "setShapesGraphs", Set.of(graph1));
		ShaclValidator.BuilderWithShapes builderWithShapes = builder.withShapes(shapesRepo.getSail());

		invoke(builder, "setShapesGraphs", Set.of(graph2));

		ShaclValidator.ValidatorWithShapes validatorWithShapes = builderWithShapes.build();
		ValidationReport report = validatorWithShapes.validate(dataRepo.getSail());
		assertTrue(report.conforms(), "builder changes should not affect an already created BuilderWithShapes");
	}

	@Test
	void settingsFromShaclSailAreCopied() throws Exception {
		ShaclSail shaclSail = new ShaclSail(new MemoryStore());

		IRI graph = iri("http://example.com/graph1");
		shaclSail.setShapesGraphs(Set.of(graph));
		shaclSail.setValidationResultsLimitTotal(1);

		ShaclValidator.Builder builder = ShaclValidator.Builder.settingsFrom(shaclSail);
		SailRepository shapesRepo = new SailRepository(new MemoryStore());
		addManyViolationsShape(shapesRepo, graph);

		SailRepository dataRepo = new SailRepository(new MemoryStore());
		addManyViolationsData(dataRepo, 3);

		ValidationReport report = builder.build().validate(dataRepo.getSail(), shapesRepo.getSail());
		assertFalse(report.conforms());
		assertTrue(report.isTruncated());
		assertEquals(1, report.getValidationResult().size());
	}

	@Test
	void rdfsSubClassReasoningSettingAffectsTargetClass() throws Exception {
		SailRepository shapesRepo = new SailRepository(new MemoryStore());
		IRI shapesGraph = RDF4J.SHACL_SHAPE_GRAPH;
		addTargetClassShape(shapesRepo, shapesGraph);

		SailRepository dataRepo = new SailRepository(new MemoryStore());
		addSubClassData(dataRepo);

		ShaclValidator.Builder builder = ShaclValidator.builder();
		invoke(builder, "setShapesGraphs", Set.of(shapesGraph));
		invoke(builder, "setRdfsSubClassReasoning", false);

		ValidationReport reportWithoutReasoning = builder.build().validate(dataRepo.getSail(), shapesRepo.getSail());
		assertTrue(reportWithoutReasoning.conforms(), "without RDFS reasoning, the targetClass should not match");

		invoke(builder, "setRdfsSubClassReasoning", true);
		ValidationReport reportWithReasoning = builder.build().validate(dataRepo.getSail(), shapesRepo.getSail());
		assertFalse(reportWithReasoning.conforms(), "with RDFS reasoning, the targetClass should match via subclass");
	}

	@Test
	void validationResultsLimitsAreApplied() throws Exception {
		SailRepository shapesRepo = new SailRepository(new MemoryStore());
		IRI shapesGraph = RDF4J.SHACL_SHAPE_GRAPH;
		addManyViolationsShape(shapesRepo, shapesGraph);

		SailRepository dataRepo = new SailRepository(new MemoryStore());
		addManyViolationsData(dataRepo, 3);

		ShaclValidator.Builder builder = ShaclValidator.builder();
		invoke(builder, "setShapesGraphs", Set.of(shapesGraph));
		invoke(builder, "setValidationResultsLimitTotal", 100L);
		invoke(builder, "setValidationResultsLimitPerConstraint", 1L);

		ValidationReport report = builder.build().validate(dataRepo.getSail(), shapesRepo.getSail());
		assertFalse(report.conforms());
		assertTrue(report.isTruncated());
		assertEquals(1, report.getValidationResult().size());
	}

	@Test
	void fromShaclSailUsesItsShapesAndCopiedSettings() throws Exception {
		ShaclSail shaclSail = new ShaclSail(new MemoryStore());
		shaclSail.setValidationResultsLimitTotal(1);

		SailRepository shaclRepository = new SailRepository(shaclSail);
		try {
			addManyViolationsShape(shaclRepository, RDF4J.SHACL_SHAPE_GRAPH);

			SailRepository dataRepo = new SailRepository(new MemoryStore());
			addManyViolationsData(dataRepo, 3);

			ValidationReport report = ShaclValidator.from(shaclSail)
					.build()
					.validate(dataRepo.getSail());

			assertFalse(report.conforms());
			assertTrue(report.isTruncated());
			assertEquals(1, report.getValidationResult().size());
		} finally {
			shaclRepository.shutDown();
		}
	}

	@Test
	@Timeout(5)
	void validationTimeoutMillisAbortsValidation() throws Exception {
		ShaclValidator.Builder builder = ShaclValidator.builder();
		invoke(builder, "setValidationTimeoutMillis", 50L);

		Sail dataRepo = new MemoryStore();
		Sail shapesRepo = new BlockingSail();

		SailException exception = assertThrows(SailException.class,
				() -> builder.build().validate(dataRepo, shapesRepo));
		assertTrue(exception.getMessage().contains("timed out"), "Expected a validation-timeout error");
	}

	@Test
	void allSettingsAreCopiedFromBuilderToValidatorAndUnaffectedByLaterMutations() throws Exception {
		IRI graph1 = iri("http://example.com/graph1");
		IRI graph2 = iri("http://example.com/graph2");

		ShaclValidator.Builder builder = ShaclValidator.builder()
				.shapeContexts(graph1, graph2)
				.setParallelValidation(false)
				.setLogValidationPlans(true)
				.setLogValidationViolations(true)
				.setGlobalLogValidationExecution(true)
				.setCacheSelectNodes(false)
				.setRdfsSubClassReasoning(false)
				.setPerformanceLogging(true)
				.setSerializableValidation(false)
				.setEclipseRdf4jShaclExtensions(true)
				.setDashDataShapes(true)
				.setValidationResultsLimitTotal(123L)
				.setValidationResultsLimitPerConstraint(45L)
				.setTransactionalValidationLimit(67L)
				.setValidationTimeoutMillis(89L)
				.disableValidation();

		ShaclValidator.Validator validator = builder.build();

		// mutate builder after creating the validator
		builder.shapeContexts(RDF4J.SHACL_SHAPE_GRAPH)
				.setParallelValidation(true)
				.setLogValidationPlans(false)
				.setLogValidationViolations(false)
				.setGlobalLogValidationExecution(false)
				.setCacheSelectNodes(true)
				.setRdfsSubClassReasoning(true)
				.setPerformanceLogging(false)
				.setSerializableValidation(true)
				.setEclipseRdf4jShaclExtensions(false)
				.setDashDataShapes(false)
				.setValidationResultsLimitTotal(999L)
				.setValidationResultsLimitPerConstraint(999L)
				.setTransactionalValidationLimit(999L)
				.setValidationTimeoutMillis(999L)
				.enableValidation();

		Object validatorBuilder = getFieldValue(validator, "builder");

		assertArrayEquals(new Resource[] { graph1, graph2 },
				(Resource[]) getFieldValue(validatorBuilder, "shapeContexts"));
		assertEquals(false, getFieldValue(validatorBuilder, "parallelValidation"));
		assertEquals(true, getFieldValue(validatorBuilder, "logValidationPlans"));
		assertEquals(true, getFieldValue(validatorBuilder, "logValidationViolations"));
		assertEquals(false, getFieldValue(validatorBuilder, "validationEnabled"));
		assertEquals(false, getFieldValue(validatorBuilder, "cacheSelectNodes"));
		assertEquals(true, getFieldValue(validatorBuilder, "globalLogValidationExecution"));
		assertEquals(false, getFieldValue(validatorBuilder, "rdfsSubClassReasoning"));
		assertEquals(true, getFieldValue(validatorBuilder, "performanceLogging"));
		assertEquals(false, getFieldValue(validatorBuilder, "serializableValidation"));
		assertEquals(true, getFieldValue(validatorBuilder, "eclipseRdf4jShaclExtensions"));
		assertEquals(true, getFieldValue(validatorBuilder, "dashDataShapes"));
		assertEquals(123L, getFieldValue(validatorBuilder, "validationResultsLimitTotal"));
		assertEquals(45L, getFieldValue(validatorBuilder, "validationResultsLimitPerConstraint"));
		assertEquals(67L, getFieldValue(validatorBuilder, "transactionalValidationLimit"));
		assertEquals(89L, getFieldValue(validatorBuilder, "validationTimeoutMillis"));
	}

	@Test
	void allSettingsAreCopiedFromBuilderToBuilderWithShapesToValidatorWithShapesAndUnaffectedByLaterMutations()
			throws Exception {
		SailRepository shapesRepo = new SailRepository(new MemoryStore());
		Sail shapesSail = shapesRepo.getSail();

		IRI graph1 = iri("http://example.com/graph1");
		IRI graph2 = iri("http://example.com/graph2");

		ShaclValidator.Builder builder = ShaclValidator.builder()
				.shapeContexts(graph1, graph2)
				.setParallelValidation(false)
				.setLogValidationPlans(true)
				.setLogValidationViolations(true)
				.setGlobalLogValidationExecution(true)
				.setCacheSelectNodes(false)
				.setRdfsSubClassReasoning(false)
				.setPerformanceLogging(true)
				.setSerializableValidation(false)
				.setEclipseRdf4jShaclExtensions(true)
				.setDashDataShapes(true)
				.setValidationResultsLimitTotal(123L)
				.setValidationResultsLimitPerConstraint(45L)
				.setTransactionalValidationLimit(67L)
				.setValidationTimeoutMillis(89L)
				.disableValidation();

		ShaclValidator.BuilderWithShapes builderWithShapes = builder.withShapes(shapesSail);

		// mutate builder after creating BuilderWithShapes
		builder.shapeContexts(RDF4J.SHACL_SHAPE_GRAPH)
				.setParallelValidation(true)
				.setLogValidationPlans(false)
				.setLogValidationViolations(false)
				.setGlobalLogValidationExecution(false)
				.setCacheSelectNodes(true)
				.setRdfsSubClassReasoning(true)
				.setPerformanceLogging(false)
				.setSerializableValidation(true)
				.setEclipseRdf4jShaclExtensions(false)
				.setDashDataShapes(false)
				.setValidationResultsLimitTotal(999L)
				.setValidationResultsLimitPerConstraint(999L)
				.setTransactionalValidationLimit(999L)
				.setValidationTimeoutMillis(999L)
				.enableValidation();

		ShaclValidator.ValidatorWithShapes validatorWithShapes = builderWithShapes.build();

		// mutate BuilderWithShapes after creating ValidatorWithShapes
		builderWithShapes.shapeContexts(RDF4J.SHACL_SHAPE_GRAPH)
				.setParallelValidation(true)
				.setLogValidationPlans(false)
				.setLogValidationViolations(false)
				.setGlobalLogValidationExecution(false)
				.setCacheSelectNodes(true)
				.setRdfsSubClassReasoning(true)
				.setPerformanceLogging(false)
				.setSerializableValidation(true)
				.setEclipseRdf4jShaclExtensions(false)
				.setDashDataShapes(false)
				.setValidationResultsLimitTotal(999L)
				.setValidationResultsLimitPerConstraint(999L)
				.setTransactionalValidationLimit(999L)
				.setValidationTimeoutMillis(999L)
				.enableValidation();
		builderWithShapes.shapes = new MemoryStore();

		Object capturedBuilderWithShapes = getFieldValue(validatorWithShapes, "builderWithShapes");

		assertSame(shapesSail, getFieldValue(capturedBuilderWithShapes, "shapes"));
		assertArrayEquals(new Resource[] { graph1, graph2 },
				(Resource[]) getFieldValue(capturedBuilderWithShapes, "shapeContexts"));
		assertEquals(false, getFieldValue(capturedBuilderWithShapes, "parallelValidation"));
		assertEquals(true, getFieldValue(capturedBuilderWithShapes, "logValidationPlans"));
		assertEquals(true, getFieldValue(capturedBuilderWithShapes, "logValidationViolations"));
		assertEquals(false, getFieldValue(capturedBuilderWithShapes, "validationEnabled"));
		assertEquals(false, getFieldValue(capturedBuilderWithShapes, "cacheSelectNodes"));
		assertEquals(true, getFieldValue(capturedBuilderWithShapes, "globalLogValidationExecution"));
		assertEquals(false, getFieldValue(capturedBuilderWithShapes, "rdfsSubClassReasoning"));
		assertEquals(true, getFieldValue(capturedBuilderWithShapes, "performanceLogging"));
		assertEquals(false, getFieldValue(capturedBuilderWithShapes, "serializableValidation"));
		assertEquals(true, getFieldValue(capturedBuilderWithShapes, "eclipseRdf4jShaclExtensions"));
		assertEquals(true, getFieldValue(capturedBuilderWithShapes, "dashDataShapes"));
		assertEquals(123L, getFieldValue(capturedBuilderWithShapes, "validationResultsLimitTotal"));
		assertEquals(45L, getFieldValue(capturedBuilderWithShapes, "validationResultsLimitPerConstraint"));
		assertEquals(67L, getFieldValue(capturedBuilderWithShapes, "transactionalValidationLimit"));
		assertEquals(89L, getFieldValue(capturedBuilderWithShapes, "validationTimeoutMillis"));
	}

	@Test
	void settingsFromShaclSailCopiesAllConfigurationSettings() throws Exception {
		ShaclSail shaclSail = new ShaclSail(new MemoryStore());
		IRI graph = iri("http://example.com/graph1");

		shaclSail.setShapesGraphs(Set.of(graph));
		shaclSail.setParallelValidation(false);
		shaclSail.setLogValidationPlans(true);
		shaclSail.setLogValidationViolations(true);
		shaclSail.setGlobalLogValidationExecution(true);
		shaclSail.setCacheSelectNodes(false);
		shaclSail.setRdfsSubClassReasoning(false);
		shaclSail.setSerializableValidation(false);
		shaclSail.setPerformanceLogging(true);
		shaclSail.setEclipseRdf4jShaclExtensions(true);
		shaclSail.setDashDataShapes(true);
		shaclSail.setValidationResultsLimitTotal(123L);
		shaclSail.setValidationResultsLimitPerConstraint(45L);
		shaclSail.setTransactionalValidationLimit(67L);
		shaclSail.disableValidation();

		ShaclValidator.Builder builder = ShaclValidator.Builder.settingsFrom(shaclSail);

		assertArrayEquals(new Resource[] { graph }, (Resource[]) getFieldValue(builder, "shapeContexts"));
		assertEquals(false, getFieldValue(builder, "parallelValidation"));
		assertEquals(true, getFieldValue(builder, "logValidationPlans"));
		assertEquals(true, getFieldValue(builder, "logValidationViolations"));
		assertEquals(false, getFieldValue(builder, "validationEnabled"));
		assertEquals(false, getFieldValue(builder, "cacheSelectNodes"));
		assertEquals(true, getFieldValue(builder, "globalLogValidationExecution"));
		assertEquals(false, getFieldValue(builder, "rdfsSubClassReasoning"));
		assertEquals(true, getFieldValue(builder, "performanceLogging"));
		assertEquals(false, getFieldValue(builder, "serializableValidation"));
		assertEquals(true, getFieldValue(builder, "eclipseRdf4jShaclExtensions"));
		assertEquals(true, getFieldValue(builder, "dashDataShapes"));
		assertEquals(123L, getFieldValue(builder, "validationResultsLimitTotal"));
		assertEquals(45L, getFieldValue(builder, "validationResultsLimitPerConstraint"));
		assertEquals(67L, getFieldValue(builder, "transactionalValidationLimit"));
	}

	private static String relativeShapesTtl() {
		return "@prefix sh: <http://www.w3.org/ns/shacl#> .\n" +
				"\n" +
				"<#PersonShape> a sh:NodeShape ;\n" +
				"  sh:targetClass <#Person> ;\n" +
				"  sh:property [\n" +
				"    sh:path <#required> ;\n" +
				"    sh:minCount 1 ;\n" +
				"  ] .\n";
	}

	private static String absoluteShapesTtl() {
		return "@prefix sh: <http://www.w3.org/ns/shacl#> .\n" +
				"\n" +
				"<http://example.com/ns#PersonShape> a sh:NodeShape ;\n" +
				"  sh:targetClass <http://example.com/ns#Person> ;\n" +
				"  sh:property [\n" +
				"    sh:path <http://example.com/ns#required> ;\n" +
				"    sh:minCount 1 ;\n" +
				"  ] .\n";
	}

	private static String relativeDataTtl() {
		return "<#alice> a <#Person> .\n";
	}

	private static String absoluteDataTtl() {
		return "<http://example.com/ns#alice> a <http://example.com/ns#Person> .\n";
	}

	private static Path writeShapesFile(Path tempDir, String shapesTtl) throws Exception {
		Path shapesFile = tempDir.resolve("shapes.ttl");
		Files.writeString(shapesFile, shapesTtl, StandardCharsets.UTF_8);
		return shapesFile;
	}

	private static Path writeDataFile(Path tempDir, String dataTtl) throws Exception {
		Path dataFile = tempDir.resolve("data.ttl");
		Files.writeString(dataFile, dataTtl, StandardCharsets.UTF_8);
		return dataFile;
	}

	private static SailRepository createDataRepoForBaseUri(String baseUri) throws Exception {
		SailRepository dataRepo = new SailRepository(new MemoryStore());
		try (SailRepositoryConnection conn = dataRepo.getConnection()) {
			conn.add(iri(baseUri + "#alice"), RDF.TYPE, iri(baseUri + "#Person"));
		}
		return dataRepo;
	}

	private static SailRepository createDataRepoForAbsoluteShapes() throws Exception {
		return createDataRepoForBaseUri("http://example.com/ns");
	}

	private static SailRepository createShapesRepoForBaseUri(String baseUri) throws Exception {
		SailRepository shapesRepo = new SailRepository(new MemoryStore());
		try (SailRepositoryConnection conn = shapesRepo.getConnection()) {
			conn.add(new StringReader(relativeShapesTtl()), baseUri, RDFFormat.TURTLE);
		}
		return shapesRepo;
	}

	private static SailRepository createShapesRepoForAbsoluteShapes() throws Exception {
		SailRepository shapesRepo = new SailRepository(new MemoryStore());
		try (SailRepositoryConnection conn = shapesRepo.getConnection()) {
			conn.add(new StringReader(absoluteShapesTtl()), "", RDFFormat.TURTLE);
		}
		return shapesRepo;
	}

	private static ValidationReport validateWithShapes(Object shapesSource, String baseUri, RDFFormat format,
			SailRepository dataRepo) throws Exception {
		ShaclValidator.Builder builder = ShaclValidator.builder();
		ShaclValidator.BuilderWithShapes builderWithShapes = (ShaclValidator.BuilderWithShapes) invoke(builder,
				"withShapes", shapesSource, baseUri, format);
		return builderWithShapes.build().validate(dataRepo.getSail());
	}

	private static ValidationReport validateWithShapesNoBaseUri(Object shapesSource, RDFFormat format,
			SailRepository dataRepo) throws Exception {
		ShaclValidator.Builder builder = ShaclValidator.builder();
		ShaclValidator.BuilderWithShapes builderWithShapes = (ShaclValidator.BuilderWithShapes) invoke(builder,
				"withShapes", shapesSource, format);
		return builderWithShapes.build().validate(dataRepo.getSail());
	}

	private static ValidationReport validateWithShapesAuto(Object shapesSource, String baseUri,
			SailRepository dataRepo) throws Exception {
		ShaclValidator.Builder builder = ShaclValidator.builder();
		ShaclValidator.BuilderWithShapes builderWithShapes = (ShaclValidator.BuilderWithShapes) invoke(builder,
				"withShapes", shapesSource, baseUri);
		return builderWithShapes.build().validate(dataRepo.getSail());
	}

	private static ValidationReport validateWithShapesAutoNoBaseUri(Object shapesSource,
			SailRepository dataRepo) throws Exception {
		ShaclValidator.Builder builder = ShaclValidator.builder();
		ShaclValidator.BuilderWithShapes builderWithShapes = (ShaclValidator.BuilderWithShapes) invoke(builder,
				"withShapes", shapesSource);
		return builderWithShapes.build().validate(dataRepo.getSail());
	}

	private static ValidationReport validateWithData(Object dataSource, String baseUri, RDFFormat format,
			String shapesTtl) throws Exception {
		ShaclValidator.ValidatorWithShapes validator = ShaclValidator.builder()
				.withShapes(shapesTtl, baseUri, RDFFormat.TURTLE)
				.build();
		return (ValidationReport) invoke(validator, "validate", dataSource, baseUri, format);
	}

	private static ValidationReport validateWithDataAuto(Object dataSource, String baseUri, String shapesTtl)
			throws Exception {
		ShaclValidator.ValidatorWithShapes validator = ShaclValidator.builder()
				.withShapes(shapesTtl, baseUri, RDFFormat.TURTLE)
				.build();
		return (ValidationReport) invoke(validator, "validate", dataSource, baseUri);
	}

	private static ValidationReport validateWithDataNoBaseUri(Object dataSource, RDFFormat format,
			String shapesTtl) throws Exception {
		ShaclValidator.ValidatorWithShapes validator = ShaclValidator.builder()
				.withShapes(shapesTtl, RDFFormat.TURTLE)
				.build();
		return (ValidationReport) invoke(validator, "validate", dataSource, format);
	}

	private static ValidationReport validateWithDataAutoNoBaseUri(Object dataSource, String shapesTtl)
			throws Exception {
		ShaclValidator.ValidatorWithShapes validator = ShaclValidator.builder()
				.withShapes(shapesTtl, RDFFormat.TURTLE)
				.build();
		return (ValidationReport) invoke(validator, "validate", dataSource);
	}

	private static ValidationReport validateWithValidatorData(Object dataSource, String baseUri, RDFFormat format,
			Sail shapesSail) throws Exception {
		ShaclValidator.Validator validator = ShaclValidator.builder().build();
		return (ValidationReport) invoke(validator, "validate", dataSource, baseUri, format, shapesSail);
	}

	private static ValidationReport validateWithValidatorDataAuto(Object dataSource, String baseUri, Sail shapesSail)
			throws Exception {
		ShaclValidator.Validator validator = ShaclValidator.builder().build();
		return (ValidationReport) invoke(validator, "validate", dataSource, baseUri, shapesSail);
	}

	private static ValidationReport validateWithValidatorDataNoBaseUri(Object dataSource, RDFFormat format,
			Sail shapesSail) throws Exception {
		ShaclValidator.Validator validator = ShaclValidator.builder().build();
		return (ValidationReport) invoke(validator, "validate", dataSource, format, shapesSail);
	}

	private static ValidationReport validateWithValidatorDataAutoNoBaseUri(Object dataSource, Sail shapesSail)
			throws Exception {
		ShaclValidator.Validator validator = ShaclValidator.builder().build();
		return (ValidationReport) invoke(validator, "validate", dataSource, shapesSail);
	}

	private static final class BlockingSail extends AbstractSail {

		private final CountDownLatch latch = new CountDownLatch(1);

		@Override
		protected SailConnection getConnectionInternal() throws SailException {
			try {
				latch.await();
				throw new AssertionError("Unexpected latch release");
			} catch (InterruptedException e) {
				Thread.currentThread().interrupt();
				throw new SailException("Interrupted while waiting for a connection", e);
			}
		}

		@Override
		protected void shutDownInternal() throws SailException {
			latch.countDown();
		}

		@Override
		public boolean isWritable() throws SailException {
			return false;
		}

		@Override
		public ValueFactory getValueFactory() {
			return SimpleValueFactory.getInstance();
		}
	}

	private static void assertHasMethod(Class<?> clazz, String name, Class<?>... parameterTypes) {
		try {
			clazz.getMethod(name, parameterTypes);
		} catch (NoSuchMethodException e) {
			fail("Expected method " + clazz.getName() + "#" + name, e);
		}
	}

	private static Object invoke(Object target, String name, Object... args) throws Exception {
		Method match = null;
		for (Method candidate : target.getClass().getMethods()) {
			if (!candidate.getName().equals(name)) {
				continue;
			}
			Class<?>[] parameterTypes = candidate.getParameterTypes();
			if (parameterTypes.length != args.length) {
				continue;
			}
			boolean compatible = true;
			for (int i = 0; i < parameterTypes.length; i++) {
				Class<?> parameterType = parameterTypes[i];
				Object arg = args[i];
				Class<?> argType = (arg == null) ? null : arg.getClass();
				if (parameterType.isPrimitive()) {
					parameterType = wrapPrimitive(parameterType);
				}
				if (argType != null && !parameterType.isAssignableFrom(argType)) {
					compatible = false;
					break;
				}
			}
			if (compatible) {
				match = candidate;
				break;
			}
		}

		if (match == null) {
			fail("Expected compatible method " + target.getClass().getName() + "#" + name);
		}

		return match.invoke(target, args);
	}

	private static Class<?> wrapPrimitive(Class<?> type) {
		if (type == boolean.class) {
			return Boolean.class;
		}
		if (type == long.class) {
			return Long.class;
		}
		if (type == int.class) {
			return Integer.class;
		}
		if (type == double.class) {
			return Double.class;
		}
		if (type == float.class) {
			return Float.class;
		}
		if (type == short.class) {
			return Short.class;
		}
		if (type == byte.class) {
			return Byte.class;
		}
		if (type == char.class) {
			return Character.class;
		}
		return type;
	}

	private static Object getFieldValue(Object target, String fieldName) {
		Class<?> clazz = target.getClass();
		while (clazz != null) {
			try {
				Field field = clazz.getDeclaredField(fieldName);
				field.setAccessible(true);
				return field.get(target);
			} catch (NoSuchFieldException e) {
				clazz = clazz.getSuperclass();
			} catch (IllegalAccessException e) {
				throw new AssertionError(e);
			}
		}
		throw new AssertionError("Field not found: " + target.getClass().getName() + "#" + fieldName);
	}

	private static void addShape(SailRepository repo, IRI graph, String requiredPropertyIri) throws Exception {
		boolean isRdf4jShapeGraph = RDF4J.SHACL_SHAPE_GRAPH.equals(graph);

		String mapping = isRdf4jShapeGraph
				? ""
				: "@prefix rdf4j: <http://rdf4j.org/schema/rdf4j#> .\n" +
						"\n" +
						"rdf4j:nil sh:shapesGraph <" + graph.stringValue() + "> .\n" +
						"\n";

		String ttl = "@prefix sh: <http://www.w3.org/ns/shacl#> .\n" +
				"@prefix ex: <http://example.com/ns#> .\n" +
				mapping +
				"ex:Shape a sh:NodeShape ;\n" +
				"  sh:targetNode ex:a ;\n" +
				"  sh:property [\n" +
				"    sh:path <" + requiredPropertyIri + "> ;\n" +
				"    sh:minCount 1 ;\n" +
				"  ] .\n";

		try (SailRepositoryConnection conn = repo.getConnection()) {
			conn.add(new StringReader(ttl), "", RDFFormat.TURTLE, graph);
		}
	}

	private static void addData(SailRepository repo, String nodeIri, String propertyIri) throws Exception {
		try (SailRepositoryConnection conn = repo.getConnection()) {
			conn.add(iri(nodeIri), iri(propertyIri), iri("http://example.com/ns#value"));
		}
	}

	private static void addManyViolationsShape(SailRepository repo, IRI graph) throws Exception {
		boolean isRdf4jShapeGraph = RDF4J.SHACL_SHAPE_GRAPH.equals(graph);

		String mapping = isRdf4jShapeGraph
				? ""
				: "@prefix rdf4j: <http://rdf4j.org/schema/rdf4j#> .\n" +
						"\n" +
						"rdf4j:nil sh:shapesGraph <" + graph.stringValue() + "> .\n" +
						"\n";

		String ttl = "@prefix sh: <http://www.w3.org/ns/shacl#> .\n" +
				"@prefix ex: <http://example.com/ns#> .\n" +
				mapping +
				"ex:Shape a sh:NodeShape ;\n" +
				"  sh:targetClass ex:Person ;\n" +
				"  sh:property [\n" +
				"    sh:path ex:required ;\n" +
				"    sh:minCount 1 ;\n" +
				"  ] .\n";
		try (SailRepositoryConnection conn = repo.getConnection()) {
			conn.add(new StringReader(ttl), "", RDFFormat.TURTLE, graph);
		}
	}

	private static void addManyViolationsData(SailRepository repo, int count) throws Exception {
		try (SailRepositoryConnection conn = repo.getConnection()) {
			for (int i = 0; i < count; i++) {
				conn.add(iri("http://example.com/ns#n" + i), RDF.TYPE, iri("http://example.com/ns#Person"));
			}
		}
	}

	private static void addTargetClassShape(SailRepository repo, IRI graph) throws Exception {
		String ttl = "@prefix sh: <http://www.w3.org/ns/shacl#> .\n" +
				"@prefix ex: <http://example.com/ns#> .\n" +
				"\n" +
				"ex:Shape a sh:NodeShape ;\n" +
				"  sh:targetClass ex:Parent ;\n" +
				"  sh:property [\n" +
				"    sh:path ex:required ;\n" +
				"    sh:minCount 1 ;\n" +
				"  ] .\n";
		try (SailRepositoryConnection conn = repo.getConnection()) {
			conn.add(new StringReader(ttl), "", RDFFormat.TURTLE, graph);
		}
	}

	private static void addSubClassData(SailRepository repo) throws Exception {
		try (SailRepositoryConnection conn = repo.getConnection()) {
			conn.add(iri("http://example.com/ns#Child"), RDFS.SUBCLASSOF, iri("http://example.com/ns#Parent"));
			conn.add(iri("http://example.com/ns#inst"), RDF.TYPE, iri("http://example.com/ns#Child"));
		}
	}

	private static IRI iri(String iri) {
		return org.eclipse.rdf4j.model.util.Values.iri(iri);
	}
}