package de.gecheckt.pdf.umbenenner.bootstrap;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.InputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;

/**
 * Executable JAR smoke tests verifying that the shaded JAR starts correctly.
 * <p>
 * These tests verify that the shaded executable JAR can be run via {@code java -jar}
 * and behaves correctly for both success and invalid configuration scenarios.
 * Regression scenarios included:
 * <ul>
 *   <li>Headless start without {@code --config}: uses default path, exits 0</li>
 *   <li>Headless start with valid {@code --config}: uses supplied path, exits 0</li>
 *   <li>Headless start with non-existent {@code --config}: hard startup failure, exits 1</li>
 *   <li>Invalid configuration: controlled failure, exits 1</li>
 *   <li>Headless output free of JavaFX initialisation markers</li>
 * </ul>
 * <p>
 * Tests are executed by the maven-failsafe-plugin after the package phase.
 * The *IT suffix ensures failsafe picks them up as integration tests.
 */
class ExecutableJarSmokeTestIT {

    private static final String JAVA_EXECUTABLE = "java";
    private static final long PROCESS_TIMEOUT_MS = 30000;

    /**
     * Success-path smoke test: verifies the JAR starts successfully with valid configuration.
     * <p>
     * Verifies:
     * - The shaded JAR file exists
     * - java -jar executes successfully
     * - Exit code is 0
     * - Configuration is loaded successfully
     * - Logging produces output
     * - Process ends in a controlled way
     * - No functional PDF processing occurs
     */
    @Test
    void jar_startsSuccessfullyWithValidConfiguration(@TempDir Path workDir) throws Exception {
        // Create runtime fixtures in the temporary working directory
        Path configDir = Files.createDirectory(workDir.resolve("config"));
        Path sourceDir = Files.createDirectory(workDir.resolve("source"));
        Path targetDir = Files.createDirectory(workDir.resolve("target"));
        Path logsDir = Files.createDirectory(workDir.resolve("logs"));
        Path dbParent = Files.createDirectory(workDir.resolve("data"));
        Path promptDir = Files.createDirectory(workDir.resolve("config/prompts"));

        Path sqliteFile = Files.createFile(dbParent.resolve("pdf-umbenenner.db"));
        Path promptTemplateFile = Files.createFile(promptDir.resolve("template.txt"));
        Files.writeString(promptTemplateFile, "Test prompt template for smoke test.");

        Path configFile = configDir.resolve("application.properties");
        String validConfig = """
                source.folder=%s
                target.folder=%s
                sqlite.file=%s
                ai.provider.active=openai-compatible
                ai.provider.openai-compatible.baseUrl=http://localhost:8080/api
                ai.provider.openai-compatible.model=gpt-4o-mini
                ai.provider.openai-compatible.timeoutSeconds=30
                ai.provider.openai-compatible.apiKey=test-api-key-for-smoke-test
                max.retries.transient=3
                max.pages=10
                max.text.characters=5000
                prompt.template.file=%s
                runtime.lock.file=%s/lock.pid
                log.directory=%s
                log.level=INFO
                """.formatted(
                sourceDir.toAbsolutePath(),
                targetDir.toAbsolutePath(),
                sqliteFile.toAbsolutePath(),
                promptTemplateFile.toAbsolutePath(),
                workDir.toAbsolutePath(),
                logsDir.toAbsolutePath()
        );
        Files.writeString(configFile, validConfig);

        // Find the shaded JAR - look in target directory relative to project root
        Path projectRoot = Paths.get(System.getProperty("user.dir"));
        Path bootstrapTarget = projectRoot.resolve("pdf-umbenenner-bootstrap/target");

        if (!Files.exists(bootstrapTarget)) {
            // Fallback: try relative from current execution context
            bootstrapTarget = Paths.get("target");
        }

        assertTrue(Files.exists(bootstrapTarget), "Bootstrap target directory must exist: " + bootstrapTarget);

        File[] jars = bootstrapTarget.toFile().listFiles((dir, name) -> 
            name.endsWith(".jar") && !name.contains("original") && !name.contains("tests")
        );
        
        assertNotNull(jars, "JAR files should exist in target directory");
        assertTrue(jars.length > 0, "At least one JAR should exist in target directory");

        Path shadedJar = Paths.get(jars[0].getAbsolutePath());
        // Prefer the shaded JAR if multiple exist
        for (File jar : jars) {
            if (jar.getName().contains("shaded") || jar.getName().equals("pdf-umbenenner-bootstrap-0.0.1-SNAPSHOT.jar")) {
                shadedJar = jar.toPath().toAbsolutePath();
                break;
            }
        }

        assertTrue(Files.exists(shadedJar), "Shaded JAR file must exist: " + shadedJar);

        // Build the java -jar command — use --headless because the GUI is not yet functional
        List<String> command = new ArrayList<>();
        command.add(JAVA_EXECUTABLE);
        command.add("-jar");
        command.add(shadedJar.toString());
        command.add("--headless");

        // Run the process in the work directory where config/application.properties will be found
        ProcessBuilder pb = new ProcessBuilder(command);
        pb.directory(workDir.toFile());
        pb.redirectErrorStream(true);

        System.out.println("[SMOKE-TEST] JAR path: " + shadedJar.toAbsolutePath());
        System.out.println("[SMOKE-TEST] Working directory: " + workDir.toAbsolutePath());
        System.out.println("[SMOKE-TEST] Command: " + String.join(" ", command));

        ProcessResult result = runProcess(pb, PROCESS_TIMEOUT_MS);

        System.out.println("[SMOKE-TEST] Exit code: " + result.exitCode());
        System.out.println("[SMOKE-TEST] Subprocess stdout/stderr:\n" + result.output());

        assertTrue(result.completed(), "Process should complete within " + PROCESS_TIMEOUT_MS + "ms timeout");
        assertEquals(0, result.exitCode(), "Successful startup should return exit code 0. Output was: " + result.output());

        // Verify logging output was produced (check console output)
        assertTrue(
                result.output().contains("Starting") ||
                result.output().contains("Bootstrap") ||
                result.output().contains("completed") ||
                result.output().contains("successfully"),
                "Output should contain startup/shutdown indicators. Got: " + result.output()
        );

        // Verify no unexpected artifacts were created beyond our fixtures
        // Count top-level entries - should only be our created directories
        long fileCount = Files.list(workDir).count();
        assertTrue(fileCount <= 7, "No extra files should be created beyond fixtures. Found: " + fileCount + " entries");
    }

    /**
     * Invalid-configuration smoke test: verifies controlled failure with exit code 1.
     * <p>
     * Verifies:
     * - java -jar runs against invalid configuration
     * - Exit code is 1
     * - Startup fails before any processing
     * - Failure is controlled (not a crash/hang)
     * - Error output indicates configuration validation failure
     */
    @Test
    void jar_failsControlledWithInvalidConfiguration(@TempDir Path workDir) throws Exception {
        // Create runtime fixtures with INVALID configuration
        Path configDir = Files.createDirectory(workDir.resolve("config"));
        Path sourceDir = Files.createDirectory(workDir.resolve("source"));
        // Intentionally missing target folder - this should cause validation failure

        Path dbParent = Files.createDirectory(workDir.resolve("data"));
        Path promptDir = Files.createDirectory(workDir.resolve("config/prompts"));

        Path sqliteFile = Files.createFile(dbParent.resolve("pdf-umbenenner.db"));
        Path promptTemplateFile = Files.createFile(promptDir.resolve("template.txt"));
        Files.writeString(promptTemplateFile, "Test prompt template.");

        // Write INVALID application.properties (missing required target.folder)
        Path configFile = configDir.resolve("application.properties");
        String invalidConfig = """
                source.folder=%s
                # target.folder is intentionally missing - should cause validation failure
                sqlite.file=%s
                ai.provider.active=openai-compatible
                ai.provider.openai-compatible.baseUrl=http://localhost:8080/api
                ai.provider.openai-compatible.model=gpt-4o-mini
                ai.provider.openai-compatible.timeoutSeconds=30
                ai.provider.openai-compatible.apiKey=test-api-key
                max.retries.transient=3
                max.pages=10
                max.text.characters=5000
                prompt.template.file=%s
                log.directory=%s/logs
                log.level=INFO
                """.formatted(
                sourceDir.toAbsolutePath(),
                sqliteFile.toAbsolutePath(),
                promptTemplateFile.toAbsolutePath(),
                workDir.toAbsolutePath()
        );
        Files.writeString(configFile, invalidConfig);

        // Find the shaded JAR
        Path projectRoot = Paths.get(System.getProperty("user.dir"));
        Path bootstrapTarget = projectRoot.resolve("pdf-umbenenner-bootstrap/target");

        if (!Files.exists(bootstrapTarget)) {
            bootstrapTarget = Paths.get("target");
        }

        assertTrue(Files.exists(bootstrapTarget), "Bootstrap target directory must exist: " + bootstrapTarget);

        File[] jars = bootstrapTarget.toFile().listFiles((dir, name) -> 
            name.endsWith(".jar") && !name.contains("original") && !name.contains("tests")
        );
        
        assertNotNull(jars, "JAR files should exist in target directory");
        assertTrue(jars.length > 0, "At least one JAR should exist");

        Path shadedJar = Paths.get(jars[0].getAbsolutePath());
        for (File jar : jars) {
            if (jar.getName().contains("shaded") || jar.getName().equals("pdf-umbenenner-bootstrap-0.0.1-SNAPSHOT.jar")) {
                shadedJar = jar.toPath().toAbsolutePath();
                break;
            }
        }

        assertTrue(Files.exists(shadedJar), "Shaded JAR file must exist: " + shadedJar);

        // Build the java -jar command — use --headless because the GUI is not yet functional
        List<String> command = new ArrayList<>();
        command.add(JAVA_EXECUTABLE);
        command.add("-jar");
        command.add(shadedJar.toString());
        command.add("--headless");

        // Run the process
        ProcessBuilder pb = new ProcessBuilder(command);
        pb.directory(workDir.toFile());
        pb.redirectErrorStream(true);

        System.out.println("[SMOKE-TEST-INVALID] JAR path: " + shadedJar.toAbsolutePath());
        System.out.println("[SMOKE-TEST-INVALID] Working directory: " + workDir.toAbsolutePath());
        System.out.println("[SMOKE-TEST-INVALID] Command: " + String.join(" ", command));

        ProcessResult result = runProcess(pb, PROCESS_TIMEOUT_MS);

        System.out.println("[SMOKE-TEST-INVALID] Exit code: " + result.exitCode());
        System.out.println("[SMOKE-TEST-INVALID] Subprocess stdout/stderr:\n" + result.output());

        assertTrue(result.completed(), "Process should complete within timeout even on failure");
        assertEquals(1, result.exitCode(), "Invalid configuration should return exit code 1. Output was: " + result.output());

        // Verify error output indicates configuration failure
        assertTrue(
                result.output().toLowerCase().contains("config") ||
                        result.output().toLowerCase().contains("validation") ||
                        result.output().toLowerCase().contains("invalid") ||
                        result.output().toLowerCase().contains("error") ||
                        result.output().toLowerCase().contains("failed"),
                "Output should indicate configuration/validation error. Got: " + result.output()
        );
    }

    // =========================================================================
    // Regression: headless start with explicit valid --config path
    // =========================================================================

    /**
     * Regression test: {@code --headless --config <valid-path>} must locate the supplied
     * configuration file and complete successfully with exit code 0.
     * <p>
     * This guards against a regression where the explicit config path would be silently ignored
     * and the default path would be used instead (or vice-versa causing a startup failure).
     */
    @Test
    void jar_headlessWithExplicitValidConfigPath_exitCode0(@TempDir Path workDir) throws Exception {
        Path configDir = Files.createDirectory(workDir.resolve("mycfg"));
        Path sourceDir = Files.createDirectory(workDir.resolve("source"));
        Path targetDir = Files.createDirectory(workDir.resolve("target"));
        Path logsDir   = Files.createDirectory(workDir.resolve("logs"));
        Path dbParent  = Files.createDirectory(workDir.resolve("data"));
        Path promptDir = Files.createDirectory(workDir.resolve("mycfg/prompts"));

        Path sqliteFile        = Files.createFile(dbParent.resolve("pdf-umbenenner.db"));
        Path promptTemplateFile = Files.createFile(promptDir.resolve("template.txt"));
        Files.writeString(promptTemplateFile, "Test prompt template for smoke test.");

        // Store the config in a non-default location (not config/application.properties)
        Path configFile = configDir.resolve("custom.properties");
        String validConfig = """
                source.folder=%s
                target.folder=%s
                sqlite.file=%s
                ai.provider.active=openai-compatible
                ai.provider.openai-compatible.baseUrl=http://localhost:8080/api
                ai.provider.openai-compatible.model=gpt-4o-mini
                ai.provider.openai-compatible.timeoutSeconds=30
                ai.provider.openai-compatible.apiKey=test-api-key-for-smoke-test
                max.retries.transient=3
                max.pages=10
                max.text.characters=5000
                prompt.template.file=%s
                runtime.lock.file=%s/lock.pid
                log.directory=%s
                log.level=INFO
                """.formatted(
                sourceDir.toAbsolutePath(),
                targetDir.toAbsolutePath(),
                sqliteFile.toAbsolutePath(),
                promptTemplateFile.toAbsolutePath(),
                workDir.toAbsolutePath(),
                logsDir.toAbsolutePath()
        );
        Files.writeString(configFile, validConfig);

        Path shadedJar = findShadedJar();

        List<String> command = new ArrayList<>();
        command.add(JAVA_EXECUTABLE);
        command.add("-jar");
        command.add(shadedJar.toString());
        command.add("--headless");
        command.add("--config");
        command.add(configFile.toAbsolutePath().toString());

        // Run in a fresh empty directory — no default config/application.properties present,
        // so the process must load via the explicit --config path.
        Path emptyWorkDir = Files.createDirectory(workDir.resolve("empty-cwd"));
        ProcessBuilder pb = new ProcessBuilder(command);
        pb.directory(emptyWorkDir.toFile());
        pb.redirectErrorStream(true);

        System.out.println("[SMOKE-TEST-EXPLICIT-CONFIG] Command: " + String.join(" ", command));

        ProcessResult result = runProcess(pb, PROCESS_TIMEOUT_MS);

        System.out.println("[SMOKE-TEST-EXPLICIT-CONFIG] Exit code: " + result.exitCode());
        System.out.println("[SMOKE-TEST-EXPLICIT-CONFIG] Output:\n" + result.output());

        assertTrue(result.completed(), "Process should complete within timeout");
        assertEquals(0, result.exitCode(),
                "Headless start with explicit valid --config path must exit 0. Output: " + result.output());
    }

    // =========================================================================
    // Regression: headless start with non-existent --config path → hard failure
    // =========================================================================

    /**
     * Regression test: {@code --headless --config <non-existent-path>} must be treated as
     * a hard startup error and produce exit code 1.
     * <p>
     * According to the startup semantics, a missing {@code --config} target in headless mode
     * is never a silent fallback but always a configuration error that blocks the run.
     * The output must contain a keyword that helps the operator diagnose the root cause.
     */
    @Test
    void jar_headlessWithNonExistentConfigPath_exitCode1(@TempDir Path workDir) throws Exception {
        Path shadedJar = findShadedJar();

        String missingPath = workDir.resolve("does-not-exist.properties").toAbsolutePath().toString();

        List<String> command = new ArrayList<>();
        command.add(JAVA_EXECUTABLE);
        command.add("-jar");
        command.add(shadedJar.toString());
        command.add("--headless");
        command.add("--config");
        command.add(missingPath);

        ProcessBuilder pb = new ProcessBuilder(command);
        pb.directory(workDir.toFile());
        pb.redirectErrorStream(true);

        System.out.println("[SMOKE-TEST-MISSING-CONFIG] Command: " + String.join(" ", command));

        ProcessResult result = runProcess(pb, PROCESS_TIMEOUT_MS);

        System.out.println("[SMOKE-TEST-MISSING-CONFIG] Exit code: " + result.exitCode());
        System.out.println("[SMOKE-TEST-MISSING-CONFIG] Output:\n" + result.output());

        assertTrue(result.completed(), "Process should complete within timeout");
        assertEquals(1, result.exitCode(),
                "Headless start with non-existent --config path must exit 1. Output: " + result.output());

        // Verify that the output contains a diagnostic keyword so operators can trace the cause.
        // Only stable keywords are checked; exact message text may evolve.
        assertTrue(
                result.output().toLowerCase().contains("not found")
                        || result.output().toLowerCase().contains("does not exist")
                        || result.output().toLowerCase().contains("missing")
                        || result.output().toLowerCase().contains("error")
                        || result.output().toLowerCase().contains("config"),
                "Output must contain a diagnostic keyword for the missing config file. Got: " + result.output()
        );
    }

    // =========================================================================
    // Regression: headless output must be free of JavaFX initialisation markers
    // =========================================================================

    /**
     * Regression test verifying that a headless start does not trigger JavaFX initialisation.
     * <p>
     * The headless path must not require a JavaFX runtime. This test runs the JAR in headless
     * mode and checks that no JavaFX-specific strings appear in the combined stdout/stderr.
     * Strings checked: "JavaFX", "Platform.startup", "Monocle", "javafx".
     * <p>
     * This is a process-level complement to the unit-level proof in
     * {@link BootstrapRunnerStartupDispatchTest#headlessStart_doesNotInvokeGuiAdapterFactory()}.
     * That unit test proves the GuiAdapterFactory is never called; this test proves the
     * observable JAR output is likewise free of JavaFX initialisation noise.
     */
    @Test
    void jar_headlessStart_outputFreeOfJavaFxInitialisationMarkers(@TempDir Path workDir) throws Exception {
        Path configDir  = Files.createDirectory(workDir.resolve("config"));
        Path sourceDir  = Files.createDirectory(workDir.resolve("source"));
        Path targetDir  = Files.createDirectory(workDir.resolve("target"));
        Path logsDir    = Files.createDirectory(workDir.resolve("logs"));
        Path dbParent   = Files.createDirectory(workDir.resolve("data"));
        Path promptDir  = Files.createDirectory(workDir.resolve("config/prompts"));

        Path sqliteFile         = Files.createFile(dbParent.resolve("pdf-umbenenner.db"));
        Path promptTemplateFile = Files.createFile(promptDir.resolve("template.txt"));
        Files.writeString(promptTemplateFile, "Test prompt template for headless JavaFX-freedom check.");

        Path configFile = configDir.resolve("application.properties");
        String validConfig = """
                source.folder=%s
                target.folder=%s
                sqlite.file=%s
                ai.provider.active=openai-compatible
                ai.provider.openai-compatible.baseUrl=http://localhost:8080/api
                ai.provider.openai-compatible.model=gpt-4o-mini
                ai.provider.openai-compatible.timeoutSeconds=30
                ai.provider.openai-compatible.apiKey=test-api-key-javafx-check
                max.retries.transient=3
                max.pages=10
                max.text.characters=5000
                prompt.template.file=%s
                runtime.lock.file=%s/lock.pid
                log.directory=%s
                log.level=INFO
                """.formatted(
                sourceDir.toAbsolutePath(),
                targetDir.toAbsolutePath(),
                sqliteFile.toAbsolutePath(),
                promptTemplateFile.toAbsolutePath(),
                workDir.toAbsolutePath(),
                logsDir.toAbsolutePath()
        );
        Files.writeString(configFile, validConfig);

        Path shadedJar = findShadedJar();

        List<String> command = new ArrayList<>();
        command.add(JAVA_EXECUTABLE);
        command.add("-jar");
        command.add(shadedJar.toString());
        command.add("--headless");

        ProcessBuilder pb = new ProcessBuilder(command);
        pb.directory(workDir.toFile());
        pb.redirectErrorStream(true);

        System.out.println("[SMOKE-TEST-JAVAFX-FREEDOM] Command: " + String.join(" ", command));

        ProcessResult result = runProcess(pb, PROCESS_TIMEOUT_MS);

        System.out.println("[SMOKE-TEST-JAVAFX-FREEDOM] Exit code: " + result.exitCode());
        System.out.println("[SMOKE-TEST-JAVAFX-FREEDOM] Output:\n" + result.output());

        assertTrue(result.completed(), "Process should complete within timeout");
        assertEquals(0, result.exitCode(),
                "Headless start must exit 0 for the JavaFX-freedom check to be meaningful. "
                + "Output: " + result.output());

        // JavaFX initialisation would produce one of these markers in stdout/stderr.
        // Their absence is the evidence that the headless path is JavaFX-free at runtime.
        assertFalse(
                result.output().contains("Platform.startup")
                        || result.output().contains("Monocle")
                        || result.output().contains("com.sun.javafx")
                        || result.output().contains("javafx.application"),
                "Headless output must not contain JavaFX initialisation markers. Got:\n" + result.output()
        );
    }

    // =========================================================================
    // Shared helper: run a process and capture output concurrently
    // =========================================================================

    /**
     * Holds the result of a subprocess execution.
     *
     * @param completed  {@code true} if the process exited within the timeout
     * @param exitCode   the process exit code (meaningful only when {@code completed} is {@code true})
     * @param output     all bytes written to stdout/stderr by the subprocess
     */
    private record ProcessResult(boolean completed, int exitCode, String output) {}

    /**
     * Starts the given {@link ProcessBuilder} and waits for the subprocess to finish,
     * draining its combined stdout/stderr concurrently to avoid pipe-buffer deadlocks.
     *
     * <p>On Windows, the default OS pipe buffer is only 4 KB. If the subprocess writes
     * more than that without the parent reading, the subprocess blocks on its next write
     * while the parent blocks in {@code waitFor} — a classic deadlock. This helper prevents
     * that by reading the subprocess output in a background thread so the pipe never fills up.
     *
     * @param pb         configured and ready-to-start {@link ProcessBuilder}; must have
     *                   {@code redirectErrorStream(true)} set so that stderr is merged into stdout
     * @param timeoutMs  maximum milliseconds to wait for the subprocess to finish
     * @return a {@link ProcessResult} containing completion status, exit code, and captured output
     * @throws Exception if the process cannot be started or the drain thread is interrupted
     */
    private ProcessResult runProcess(ProcessBuilder pb, long timeoutMs) throws Exception {
        Process process = pb.start();

        // Drain stdout/stderr in a background thread to prevent Windows pipe-buffer deadlocks.
        // The OS pipe buffer is only 4 KB on Windows; if the subprocess writes more than that
        // while the parent is blocked in waitFor(), neither side can proceed.
        ByteArrayOutputStream buffer = new ByteArrayOutputStream();
        Thread drainThread = new Thread(() -> {
            try (InputStream in = process.getInputStream()) {
                in.transferTo(buffer);
            } catch (IOException ignored) {
                // Stream closed by process exit — normal termination path
            }
        }, "subprocess-output-drain");
        drainThread.setDaemon(true);
        drainThread.start();

        boolean completed = process.waitFor(timeoutMs, TimeUnit.MILLISECONDS);
        drainThread.join(5_000); // Allow drain to finish (process has already exited or timed out)

        int exitCode = completed ? process.exitValue() : -1;
        return new ProcessResult(completed, exitCode, buffer.toString());
    }

    // =========================================================================
    // Shared helper: locate the shaded JAR
    // =========================================================================

    /**
     * Locates the shaded executable JAR produced by the {@code maven-shade-plugin}.
     * <p>
     * Searches in {@code pdf-umbenenner-bootstrap/target} relative to the Maven reactor root
     * ({@code user.dir}) first, then falls back to the local {@code target/} directory.
     *
     * @return absolute path to the shaded JAR
     * @throws AssertionError if no matching JAR can be found
     */
    private Path findShadedJar() {
        Path projectRoot     = Paths.get(System.getProperty("user.dir"));
        Path bootstrapTarget = projectRoot.resolve("pdf-umbenenner-bootstrap/target");
        if (!Files.exists(bootstrapTarget)) {
            bootstrapTarget = Paths.get("target");
        }

        assertTrue(Files.exists(bootstrapTarget),
                "Bootstrap target directory must exist: " + bootstrapTarget);

        File[] jars = bootstrapTarget.toFile().listFiles(
                (dir, name) -> name.endsWith(".jar")
                        && !name.contains("original")
                        && !name.contains("tests"));

        assertNotNull(jars, "JAR files should exist in target directory");
        assertTrue(jars.length > 0, "At least one JAR should exist in target directory");

        Path shadedJar = Paths.get(jars[0].getAbsolutePath());
        for (File jar : jars) {
            if (jar.getName().contains("shaded")
                    || jar.getName().equals("pdf-umbenenner-bootstrap-0.0.1-SNAPSHOT.jar")) {
                shadedJar = jar.toPath().toAbsolutePath();
                break;
            }
        }

        assertTrue(Files.exists(shadedJar), "Shaded JAR file must exist: " + shadedJar);
        return shadedJar;
    }
}