/*******************************************************************************
 * Copyright (C) 2026, Leo Galambos
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 *
 * 3. Neither the name of the copyright holder nor the names of its contributors
 *    may be used to endorse or promote products derived from this software
 *    without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 ******************************************************************************/
package org.egothor.stemmer;

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.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
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.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import java.util.zip.GZIPInputStream;

import org.egothor.stemmer.StemmerPatchTrieLoader.Language;
import org.egothor.stemmer.trie.CompiledNode;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import org.junit.jupiter.api.io.TempDir;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.MockedStatic;

/**
 * Professional test suite for {@link StemmerPatchTrieLoader}.
 *
 * <p>
 * The suite combines focused API-level verification with integration validation
 * against registered default-model dictionaries. It verifies:
 * </p>
 * <ul>
 * <li>all public loading overloads</li>
 * <li>binary persistence round-trips</li>
 * <li>null-argument contracts</li>
 * <li>comment-aware parsing delegated to {@link StemmerDictionaryParser}</li>
 * <li>preservation of all valid stem candidates returned by
 * {@link FrequencyTrie#getAll(String)}</li>
 * <li>the current bundled language set, including right-to-left metadata</li>
 * </ul>
 */
@Tag("integration")
@Tag("stemmer")
@Tag("io")
@Tag("parser")
@Tag("trie")
@Tag("persistence")
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@SuppressWarnings("deprecation")
final class StemmerPatchTrieLoaderTest {

    /**
     * Temporary directory for filesystem-based tests.
     */
    @TempDir
    private Path tempDir;

    /**
     * Reduction mode used for deterministic getAll-preserving checks in focused
     * tests.
     */
    private static final ReductionMode DEFAULT_REDUCTION_MODE = ReductionMode.MERGE_SUBTREES_WITH_EQUIVALENT_RANKED_GET_ALL_RESULTS;

    /**
     * Representative number of bundled words used for overload consistency checks.
     */
    private static final int REPRESENTATIVE_BUNDLED_WORD_COUNT = 25;

    /**
     * Provides arguments for registered model dictionary verification across both supported
     * getAll-preserving reduction modes.
     *
     * <p>
     * The stream is derived directly from the current {@link Language} enum so the
     * test suite follows the supported bundled language set automatically.
     * </p>
     *
     * @return parameter stream
     */
    static Stream<Arguments> bundledDictionaryCases() {
        final ReductionMode[] reductionModes = new ReductionMode[] {
                ReductionMode.MERGE_SUBTREES_WITH_EQUIVALENT_RANKED_GET_ALL_RESULTS,
                ReductionMode.MERGE_SUBTREES_WITH_EQUIVALENT_UNORDERED_GET_ALL_RESULTS };

        return Arrays.stream(StemmerPatchTrieLoader.Language.values())
                .flatMap(
                        language -> IntStream.range(0, reductionModes.length)
                                .mapToObj(index -> Arguments.of(
                                        String.format("%02d-%s-%s", index + 1, language.name().toLowerCase(),
                                                reductionModes[index].name().toLowerCase()),
                                        language, reductionModes[index])));
    }

    /**
     * Provides representative bundled languages for overload consistency checks.
     *
     * <p>
     * The sample intentionally covers both traversal directions.
     * </p>
     *
     * @return parameter stream
     */
    static Stream<Arguments> bundledLanguageSamples() {
        return Stream.of(Arguments.of("01-us_uk", StemmerPatchTrieLoader.Language.US_UK),
                Arguments.of("02-de_de", StemmerPatchTrieLoader.Language.DE_DE),
                Arguments.of("03-fa_ir", StemmerPatchTrieLoader.Language.FA_IR),
                Arguments.of("04-he_il", StemmerPatchTrieLoader.Language.HE_IL),
                Arguments.of("05-yi", StemmerPatchTrieLoader.Language.YI));
    }

    /**
     * Provides invalid null-argument scenarios for public methods.
     *
     * @return parameter stream
     */
    static Stream<Arguments> nullContractCases() {
        final ReductionSettings settings = ReductionSettings.withDefaults(DEFAULT_REDUCTION_MODE);
        final FrequencyTrie<String> trie = new FrequencyTrie.Builder<String>(String[]::new, settings)
                .put("running", PatchCommandEncoder.builder().build().encode("running", "run")).build();

        return Stream.of(
                Arguments.of("01-load-language-settings",
                        (ExecutableOperation) () -> StemmerPatchTrieLoader.load((StemmerPatchTrieLoader.Language) null,
                                true, settings),
                        "language"),
                Arguments.of("02-load-language-mode",
                        (ExecutableOperation) () -> StemmerPatchTrieLoader.load((StemmerPatchTrieLoader.Language) null,
                                true, DEFAULT_REDUCTION_MODE),
                        "language"),
                Arguments.of("03-load-language-null-settings",
                        (ExecutableOperation) () -> StemmerPatchTrieLoader.load(StemmerPatchTrieLoader.Language.US_UK,
                                true, (ReductionSettings) null),
                        "reductionSettings"),
                Arguments.of("04-load-language-null-mode",
                        (ExecutableOperation) () -> StemmerPatchTrieLoader.load(StemmerPatchTrieLoader.Language.US_UK,
                                true, (ReductionMode) null),
                        "reductionMode"),
                Arguments.of("05-load-path-settings",
                        (ExecutableOperation) () -> StemmerPatchTrieLoader.load((Path) null, true, settings), "path"),
                Arguments.of("06-load-path-mode",
                        (ExecutableOperation) () -> StemmerPatchTrieLoader.load((Path) null, true,
                                DEFAULT_REDUCTION_MODE),
                        "path"),
                Arguments.of("07-load-path-null-settings",
                        (ExecutableOperation) () -> StemmerPatchTrieLoader.load(tempPath(), true,
                                (ReductionSettings) null),
                        "reductionSettings"),
                Arguments.of("08-load-path-null-mode",
                        (ExecutableOperation) () -> StemmerPatchTrieLoader.load(tempPath(), true, (ReductionMode) null),
                        "reductionMode"),
                Arguments.of("09-load-string-settings",
                        (ExecutableOperation) () -> StemmerPatchTrieLoader.load((String) null, true, settings),
                        StemmerPatchTrieLoader.FILENAME_REQUIRED),
                Arguments.of("10-load-string-mode",
                        (ExecutableOperation) () -> StemmerPatchTrieLoader.load((String) null, true,
                                DEFAULT_REDUCTION_MODE),
                        StemmerPatchTrieLoader.FILENAME_REQUIRED),
                Arguments.of("11-load-string-null-settings",
                        (ExecutableOperation) () -> StemmerPatchTrieLoader.load(tempPath().toString(), true,
                                (ReductionSettings) null),
                        "reductionSettings"),
                Arguments.of("12-load-string-null-mode",
                        (ExecutableOperation) () -> StemmerPatchTrieLoader.load(tempPath().toString(), true,
                                (ReductionMode) null),
                        "reductionMode"),
                Arguments.of("13-load-binary-path",
                        (ExecutableOperation) () -> StemmerPatchTrieLoader.loadBinary((Path) null), "path"),
                Arguments.of("14-load-binary-string",
                        (ExecutableOperation) () -> StemmerPatchTrieLoader.loadBinary((String) null),
                        StemmerPatchTrieLoader.FILENAME_REQUIRED),
                Arguments.of("15-load-binary-path-override",
                        (ExecutableOperation) () -> StemmerPatchTrieLoader.loadBinary((Path) null, FrequencyTrie.DEFAULT_MAX_EXPANDED_INDEX),
                        "path"),
                Arguments.of("16-load-binary-string-override",
                        (ExecutableOperation) () -> StemmerPatchTrieLoader.loadBinary((String) null,
                                FrequencyTrie.DEFAULT_MAX_EXPANDED_INDEX),
                        StemmerPatchTrieLoader.FILENAME_REQUIRED),
                Arguments.of("17-load-binary-stream",
                        (ExecutableOperation) () -> StemmerPatchTrieLoader.loadBinary((InputStream) null),
                        "inputStream"),
                Arguments.of("18-save-binary-null-trie-path",
                        (ExecutableOperation) () -> StemmerPatchTrieLoader.saveBinary(null, tempPath()), "trie"),
                Arguments.of("19-save-binary-null-path",
                        (ExecutableOperation) () -> StemmerPatchTrieLoader.saveBinary(trie, (Path) null), "path"),
                Arguments.of("20-save-binary-null-trie-string",
                        (ExecutableOperation) () -> StemmerPatchTrieLoader.saveBinary(null, tempPath().toString()),
                        "trie"),
                Arguments.of("21-save-binary-null-string",
                        (ExecutableOperation) () -> StemmerPatchTrieLoader.saveBinary(trie, (String) null),
                        StemmerPatchTrieLoader.FILENAME_REQUIRED),
                Arguments.of("22-load-language-null-metadata",
                        (ExecutableOperation) () -> StemmerPatchTrieLoader.load(StemmerPatchTrieLoader.Language.US_UK,
                                true, (TrieMetadata) null),
                        "metadata"),
                Arguments.of("23-load-path-null-metadata",
                        (ExecutableOperation) () -> StemmerPatchTrieLoader.load(tempPath(), true, (TrieMetadata) null),
                        "metadata"),
                Arguments.of("24-load-string-null-metadata",
                        (ExecutableOperation) () -> StemmerPatchTrieLoader.load(tempPath().toString(), true,
                                (TrieMetadata) null),
                        "metadata"),
                Arguments.of("25-load-binary-metadata-path-null",
                        (ExecutableOperation) () -> StemmerPatchTrieLoader.loadBinaryMetadata((Path) null), "path"),
                Arguments.of("26-load-binary-metadata-string-null",
                        (ExecutableOperation) () -> StemmerPatchTrieLoader.loadBinaryMetadata((String) null),
                        StemmerPatchTrieLoader.FILENAME_REQUIRED),
                Arguments.of("27-load-binary-metadata-stream-null",
                        (ExecutableOperation) () -> StemmerPatchTrieLoader.loadBinaryMetadata((InputStream) null),
                        "inputStream"),
                Arguments.of("28-load-compiled-language-settings-null-language",
                        (ExecutableOperation) () -> StemmerPatchTrieLoader.loadCompiled(
                                (StemmerPatchTrieLoader.Language) null, true, settings),
                        "language"),
                Arguments.of("29-load-compiled-language-settings-null-settings",
                        (ExecutableOperation) () -> StemmerPatchTrieLoader.loadCompiled(
                                StemmerPatchTrieLoader.Language.US_UK, true, (ReductionSettings) null),
                        "reductionSettings"),
                Arguments.of("30-load-compiled-language-mode-null-language",
                        (ExecutableOperation) () -> StemmerPatchTrieLoader.loadCompiled(
                                (StemmerPatchTrieLoader.Language) null, true, DEFAULT_REDUCTION_MODE),
                        "language"),
                Arguments.of("31-load-compiled-language-mode-null-mode",
                        (ExecutableOperation) () -> StemmerPatchTrieLoader.loadCompiled(
                                StemmerPatchTrieLoader.Language.US_UK, true, (ReductionMode) null),
                        "reductionMode"),
                Arguments.of("32-load-compiled-language-metadata-null-metadata",
                        (ExecutableOperation) () -> StemmerPatchTrieLoader.loadCompiled(
                                StemmerPatchTrieLoader.Language.US_UK, true, (TrieMetadata) null),
                        "metadata"),
                Arguments.of("33-load-compiled-path-settings-null-path",
                        (ExecutableOperation) () -> StemmerPatchTrieLoader.loadCompiled((Path) null, true, settings),
                        "path"),
                Arguments.of("34-load-compiled-path-settings-null-settings",
                        (ExecutableOperation) () -> StemmerPatchTrieLoader.loadCompiled(tempPath(), true,
                                (ReductionSettings) null),
                        "reductionSettings"),
                Arguments.of("35-load-compiled-path-mode-null-mode",
                        (ExecutableOperation) () -> StemmerPatchTrieLoader.loadCompiled(tempPath(), true,
                                (ReductionMode) null),
                        "reductionMode"),
                Arguments.of("36-load-compiled-path-metadata-null-metadata",
                        (ExecutableOperation) () -> StemmerPatchTrieLoader.loadCompiled(tempPath(), true,
                                (TrieMetadata) null),
                        "metadata"),
                Arguments.of("37-load-compiled-string-settings-null-file",
                        (ExecutableOperation) () -> StemmerPatchTrieLoader.loadCompiled((String) null, true, settings),
                        StemmerPatchTrieLoader.FILENAME_REQUIRED),
                Arguments.of("38-load-compiled-string-settings-null-settings",
                        (ExecutableOperation) () -> StemmerPatchTrieLoader.loadCompiled(tempPath().toString(), true,
                                (ReductionSettings) null),
                        "reductionSettings"),
                Arguments.of("39-load-compiled-string-mode-null-mode",
                        (ExecutableOperation) () -> StemmerPatchTrieLoader.loadCompiled(tempPath().toString(), true,
                                (ReductionMode) null),
                        "reductionMode"),
                Arguments.of("40-load-compiled-string-metadata-null-metadata",
                        (ExecutableOperation) () -> StemmerPatchTrieLoader.loadCompiled(tempPath().toString(), true,
                                (TrieMetadata) null),
                        "metadata"),
                Arguments.of("41-load-binary-compiled-path-null",
                        (ExecutableOperation) () -> StemmerPatchTrieLoader.loadBinaryCompiled((Path) null), "path"),
                Arguments.of("42-load-binary-compiled-path-override-null",
                        (ExecutableOperation) () -> StemmerPatchTrieLoader.loadBinaryCompiled((Path) null,
                                FrequencyTrie.DEFAULT_MAX_EXPANDED_INDEX),
                        "path"),
                Arguments.of("43-load-binary-compiled-string-null",
                        (ExecutableOperation) () -> StemmerPatchTrieLoader.loadBinaryCompiled((String) null),
                        StemmerPatchTrieLoader.FILENAME_REQUIRED),
                Arguments.of("44-load-binary-compiled-string-override-null",
                        (ExecutableOperation) () -> StemmerPatchTrieLoader.loadBinaryCompiled((String) null,
                                FrequencyTrie.DEFAULT_MAX_EXPANDED_INDEX),
                        StemmerPatchTrieLoader.FILENAME_REQUIRED),
                Arguments.of("45-load-binary-compiled-stream-null",
                        (ExecutableOperation) () -> StemmerPatchTrieLoader.loadBinaryCompiled((InputStream) null),
                        "inputStream"));
    }

    /**
     * Returns a representative temporary path for null-contract method sources.
     *
     * @return representative path
     */
    private static Path tempPath() {
        return Path.of("target", "test-loader-null-contracts.dict");
    }

    /**
     * Focused API contract tests.
     */
    @Nested
    @DisplayName("API contracts")
    @Tag("validation")
    @Tag("integration")
    @Tag("trie")
    final class ApiContractTests {

        /**
         * Verifies that all documented null contracts are enforced consistently by
         * public methods.
         *
         * @param scenario                expected scenario identifier
         * @param operation               operation that must fail
         * @param expectedMessageFragment expected message fragment
         */
        @ParameterizedTest(name = "[{index}] {0}")
        @MethodSource("org.egothor.stemmer.StemmerPatchTrieLoaderTest#nullContractCases")
        @DisplayName("Public methods must reject null arguments with precise diagnostics")
        void shouldRejectNullArguments(final String scenario, final ExecutableOperation operation,
                final String expectedMessageFragment) {
            final NullPointerException exception = assertThrows(NullPointerException.class, operation::execute,
                    "Scenario " + scenario + " must reject null input.");

            assertNotNull(exception.getMessage(), "NullPointerException message must be present.");
            assertEquals(expectedMessageFragment, exception.getMessage(),
                    "Scenario " + scenario + " must identify the offending argument.");
        }

        /**
         * Verifies that loading from a missing file fails with an {@link IOException}.
         */
        @Test
        @DisplayName("Loading from a missing dictionary file must fail with IOException")
        void shouldFailWhenDictionaryFileDoesNotExist() {
            final Path missingFile = tempDir.resolve("missing-dictionary.dict");

            assertThrows(IOException.class,
                    () -> StemmerPatchTrieLoader.load(missingFile, true, DEFAULT_REDUCTION_MODE));
        }

        /**
         * Verifies that loading a missing binary file fails with an
         * {@link IOException}.
         */
        @Test
        @DisplayName("Loading a missing binary trie file must fail with IOException")
        void shouldFailWhenBinaryFileDoesNotExist() {
            final Path missingFile = tempDir.resolve("missing-trie.bin.gz");

            assertThrows(IOException.class, () -> StemmerPatchTrieLoader.loadBinary(missingFile));
        }
    }

    /**
     * Structural tests for direct compiled binary-loader delegation.
     */
    @Nested
    @DisplayName("Compiled binary routing")
    @Tag("unit")
    @Tag("io")
    @Tag("persistence")
    final class CompiledBinaryRoutingTests {

        /**
         * Verifies that the path overload returns the direct compiled BinaryIO result
         * without invoking deprecated String deserialization.
         *
         * @throws IOException if the loader unexpectedly reports an I/O failure
         */
        @Test
        @DisplayName("Path compiled loading must use direct compiled BinaryIO")
        void shouldRouteCompiledPathLoadingDirectly() throws IOException {
            final Path path = tempDir.resolve("compiled-routing-path.bin.gz");
            final FrequencyTrie<CompiledPatchCommand> compiledSentinel = compiledRoutingSentinel();
            final FrequencyTrie<String> stringFallback = stringRoutingFallback();

            try (MockedStatic<StemmerPatchTrieBinaryIO> binaryIo = mockStatic(StemmerPatchTrieBinaryIO.class)) {
                binaryIo.when(() -> StemmerPatchTrieBinaryIO.readCompiled(path)).thenReturn(compiledSentinel);
                binaryIo.when(() -> StemmerPatchTrieBinaryIO.read(path)).thenReturn(stringFallback);

                final FrequencyTrie<CompiledPatchCommand> actual = StemmerPatchTrieLoader.loadBinaryCompiled(path);

                assertSame(compiledSentinel, actual,
                        "Path loading must return the trie materialized by the direct compiled reader.");
                binaryIo.verify(() -> StemmerPatchTrieBinaryIO.readCompiled(path), times(1));
                binaryIo.verify(() -> StemmerPatchTrieBinaryIO.read(path), never());
                binaryIo.verifyNoMoreInteractions();
            }
        }

        /**
         * Verifies that the path-and-span overload preserves both arguments while
         * delegating directly to compiled BinaryIO.
         *
         * @throws IOException if the loader unexpectedly reports an I/O failure
         */
        @Test
        @DisplayName("Path and span compiled loading must use direct compiled BinaryIO")
        void shouldRouteCompiledPathAndSpanLoadingDirectly() throws IOException {
            final Path path = tempDir.resolve("compiled-routing-path-span.bin.gz");
            final int maxExpandedIndex = 17;
            final FrequencyTrie<CompiledPatchCommand> compiledSentinel = compiledRoutingSentinel();
            final FrequencyTrie<String> stringFallback = stringRoutingFallback();

            try (MockedStatic<StemmerPatchTrieBinaryIO> binaryIo = mockStatic(StemmerPatchTrieBinaryIO.class)) {
                binaryIo.when(() -> StemmerPatchTrieBinaryIO.readCompiled(path, maxExpandedIndex))
                        .thenReturn(compiledSentinel);
                binaryIo.when(() -> StemmerPatchTrieBinaryIO.read(path, maxExpandedIndex)).thenReturn(stringFallback);

                final FrequencyTrie<CompiledPatchCommand> actual = StemmerPatchTrieLoader.loadBinaryCompiled(path,
                        maxExpandedIndex);

                assertSame(compiledSentinel, actual,
                        "Path-and-span loading must return the trie materialized by the direct compiled reader.");
                binaryIo.verify(() -> StemmerPatchTrieBinaryIO.readCompiled(path, maxExpandedIndex), times(1));
                binaryIo.verify(() -> StemmerPatchTrieBinaryIO.read(path, maxExpandedIndex), never());
                binaryIo.verifyNoMoreInteractions();
            }
        }

        /**
         * Verifies that the file-name overload returns the direct compiled BinaryIO
         * result without invoking deprecated String deserialization.
         *
         * @throws IOException if the loader unexpectedly reports an I/O failure
         */
        @Test
        @DisplayName("String compiled loading must use direct compiled BinaryIO")
        void shouldRouteCompiledStringLoadingDirectly() throws IOException {
            final String fileName = tempDir.resolve("compiled-routing-string.bin.gz").toString();
            final FrequencyTrie<CompiledPatchCommand> compiledSentinel = compiledRoutingSentinel();
            final FrequencyTrie<String> stringFallback = stringRoutingFallback();

            try (MockedStatic<StemmerPatchTrieBinaryIO> binaryIo = mockStatic(StemmerPatchTrieBinaryIO.class)) {
                binaryIo.when(() -> StemmerPatchTrieBinaryIO.readCompiled(fileName)).thenReturn(compiledSentinel);
                binaryIo.when(() -> StemmerPatchTrieBinaryIO.read(fileName)).thenReturn(stringFallback);

                final FrequencyTrie<CompiledPatchCommand> actual = StemmerPatchTrieLoader
                        .loadBinaryCompiled(fileName);

                assertSame(compiledSentinel, actual,
                        "String loading must return the trie materialized by the direct compiled reader.");
                binaryIo.verify(() -> StemmerPatchTrieBinaryIO.readCompiled(fileName), times(1));
                binaryIo.verify(() -> StemmerPatchTrieBinaryIO.read(fileName), never());
                binaryIo.verifyNoMoreInteractions();
            }
        }

        /**
         * Verifies that the file-name-and-span overload preserves both arguments
         * while delegating directly to compiled BinaryIO.
         *
         * @throws IOException if the loader unexpectedly reports an I/O failure
         */
        @Test
        @DisplayName("String and span compiled loading must use direct compiled BinaryIO")
        void shouldRouteCompiledStringAndSpanLoadingDirectly() throws IOException {
            final String fileName = tempDir.resolve("compiled-routing-string-span.bin.gz").toString();
            final int maxExpandedIndex = 17;
            final FrequencyTrie<CompiledPatchCommand> compiledSentinel = compiledRoutingSentinel();
            final FrequencyTrie<String> stringFallback = stringRoutingFallback();

            try (MockedStatic<StemmerPatchTrieBinaryIO> binaryIo = mockStatic(StemmerPatchTrieBinaryIO.class)) {
                binaryIo.when(() -> StemmerPatchTrieBinaryIO.readCompiled(fileName, maxExpandedIndex))
                        .thenReturn(compiledSentinel);
                binaryIo.when(() -> StemmerPatchTrieBinaryIO.read(fileName, maxExpandedIndex))
                        .thenReturn(stringFallback);

                final FrequencyTrie<CompiledPatchCommand> actual = StemmerPatchTrieLoader
                        .loadBinaryCompiled(fileName, maxExpandedIndex);

                assertSame(compiledSentinel, actual,
                        "String-and-span loading must return the trie materialized by the direct compiled reader.");
                binaryIo.verify(() -> StemmerPatchTrieBinaryIO.readCompiled(fileName, maxExpandedIndex), times(1));
                binaryIo.verify(() -> StemmerPatchTrieBinaryIO.read(fileName, maxExpandedIndex), never());
                binaryIo.verifyNoMoreInteractions();
            }
        }

        /**
         * Verifies that the stream overload preserves stream identity while
         * delegating directly to compiled BinaryIO.
         *
         * @throws IOException if the loader unexpectedly reports an I/O failure
         */
        @Test
        @DisplayName("InputStream compiled loading must use direct compiled BinaryIO")
        void shouldRouteCompiledInputStreamLoadingDirectly() throws IOException {
            final InputStream inputStream = new ByteArrayInputStream(new byte[] { 1, 2, 3 });
            final FrequencyTrie<CompiledPatchCommand> compiledSentinel = compiledRoutingSentinel();
            final FrequencyTrie<String> stringFallback = stringRoutingFallback();

            try (MockedStatic<StemmerPatchTrieBinaryIO> binaryIo = mockStatic(StemmerPatchTrieBinaryIO.class)) {
                binaryIo.when(() -> StemmerPatchTrieBinaryIO.readCompiled(inputStream)).thenReturn(compiledSentinel);
                binaryIo.when(() -> StemmerPatchTrieBinaryIO.read(inputStream)).thenReturn(stringFallback);

                final FrequencyTrie<CompiledPatchCommand> actual = StemmerPatchTrieLoader
                        .loadBinaryCompiled(inputStream);

                assertSame(compiledSentinel, actual,
                        "Stream loading must return the trie materialized by the direct compiled reader.");
                binaryIo.verify(() -> StemmerPatchTrieBinaryIO.readCompiled(inputStream), times(1));
                binaryIo.verify(() -> StemmerPatchTrieBinaryIO.read(inputStream), never());
                binaryIo.verifyNoMoreInteractions();
            }
        }
    }

    /**
     * Focused internal loader behavior tests.
     */
    @Nested
    @DisplayName("Internal helper behavior")
    @Tag("construction")
    @Tag("integration")
    @Tag("trie")
    final class InternalLoaderBehaviorTests {

        /**
         * Verifies that bundled language loading follows explicit
         * right-to-left metadata mapping.
         */
        @Test
        @DisplayName("bundled language loading must infer traversal direction from language metadata")
        void shouldLoadBundledLanguagesUsingLanguageRightToLeftMetadata() throws IOException {
            final ReductionSettings settings = ReductionSettings.withDefaults(DEFAULT_REDUCTION_MODE);
            final FrequencyTrie<String> leftToRightDictionary = StemmerPatchTrieLoader.load(
                    StemmerPatchTrieLoader.Language.US_UK, true, settings);
            final FrequencyTrie<String> rightToLeftDictionary = StemmerPatchTrieLoader.load(
                    StemmerPatchTrieLoader.Language.FA_IR, true, settings);

            assertEquals(WordTraversalDirection.BACKWARD, leftToRightDictionary.traversalDirection(),
                    "Left-to-right languages should use backward traversal.");
            assertEquals(WordTraversalDirection.FORWARD, rightToLeftDictionary.traversalDirection(),
                    "Right-to-left languages should use forward traversal.");
        }

        /**
         * Verifies the mode-based and settings-based bundled load overloads remain
         * semantically consistent for traversal direction.
         */
        @Test
        @DisplayName("load(Language,.., ReductionMode) and load(Language,.., ReductionSettings) should agree on traversal direction")
        void shouldKeepBundledTraversalDirectionConsistentAcrossOverloads() throws IOException {
            final FrequencyTrie<String> byMode = StemmerPatchTrieLoader.load(
                    StemmerPatchTrieLoader.Language.US_UK, true, DEFAULT_REDUCTION_MODE);
            final FrequencyTrie<String> bySettings = StemmerPatchTrieLoader.load(
                    StemmerPatchTrieLoader.Language.US_UK, true,
                    ReductionSettings.withDefaults(DEFAULT_REDUCTION_MODE));

            assertEquals(byMode.traversalDirection(), bySettings.traversalDirection());
            assertEquals(byMode.metadata().reductionSettings().reductionMode(),
                    bySettings.metadata().reductionSettings().reductionMode());
        }

        /**
         * Verifies bundled resource access succeeds for known resources and fails
         * for unknown resources.
         */
        @Test
        @DisplayName("openBundledResource should return readable streams for known resources and fail for unknown ones")
        void shouldOpenBundledResourcesSuccessfullyAndRejectUnknowns() throws IOException {
            final String resourcePath = StemmerPatchTrieLoader.Language.US_UK.resourcePath();
            try (InputStream inputStream = StemmerPatchTrieLoader.openBundledResource(resourcePath);
                    BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {
                assertNotNull(reader.readLine(), "Known bundled resource must expose readable content.");
            }

            final String missingResource = "org/egothor/stemmer/missing-dictionary.dict.gz";
            final IOException exception = assertThrows(IOException.class,
                    () -> StemmerPatchTrieLoader.openBundledResource(missingResource));

            assertEquals("Stemmer resource not found: " + missingResource, exception.getMessage());
        }
    }

    /**
     * Focused filesystem and parser behavior tests.
     */
    @Nested
    @DisplayName("Filesystem and parser behavior")
    @Tag("io")
    @Tag("construction")
    @Tag("integration")
    @Tag("trie")
    final class FilesystemAndParserTests {

        /**
         * Verifies that all textual loading overloads produce equivalent tries for the
         * same source dictionary.
         *
         * @throws IOException if the test file cannot be written or read
         */
        @Test
        @DisplayName("Path and String overloads must load equivalent tries")
        void shouldLoadEquivalentTrieFromPathAndStringOverloads() throws IOException {
            final Path dictionaryFile = writeDictionary("""
                    run	running	runs	runner
                    play	playing	played	plays
                    city	cities
                    """);

            final ReductionSettings settings = ReductionSettings.withDefaults(DEFAULT_REDUCTION_MODE);

            final FrequencyTrie<String> fromPathWithSettings = StemmerPatchTrieLoader.load(dictionaryFile, true,
                    settings);
            final FrequencyTrie<String> fromPathWithMode = StemmerPatchTrieLoader.load(dictionaryFile, true,
                    DEFAULT_REDUCTION_MODE);
            final FrequencyTrie<String> fromStringWithSettings = StemmerPatchTrieLoader.load(dictionaryFile.toString(),
                    true, settings);
            final FrequencyTrie<String> fromStringWithMode = StemmerPatchTrieLoader.load(dictionaryFile.toString(),
                    true, DEFAULT_REDUCTION_MODE);

            assertTriePatchSemanticsEqual(fromPathWithSettings, fromPathWithMode, "running", "played", "cities", "run");
            assertTriePatchSemanticsEqual(fromPathWithSettings, fromStringWithSettings, "running", "played", "cities",
                    "run");
            assertTriePatchSemanticsEqual(fromPathWithSettings, fromStringWithMode, "running", "played", "cities",
                    "run");
        }

        /**
         * Verifies that textual compiled loading overloads produce patch-command
         * tries with the same stemming semantics as the canonical textual patch trie.
         *
         * @throws IOException if the test file cannot be written or read
         */
        @Test
        @DisplayName("Textual compiled load overloads must preserve stemming semantics")
        void shouldLoadCompiledTrieFromTextualOverloads() throws IOException {
            final Path dictionaryFile = writeDictionary("""
                    run	running	runs	runner
                    play	playing	played	plays
                    city	cities
                    """);

            final ReductionSettings settings = ReductionSettings.withDefaults(DEFAULT_REDUCTION_MODE);
            final FrequencyTrie<String> expected = StemmerPatchTrieLoader.load(dictionaryFile, true, settings);
            final FrequencyTrie<CompiledPatchCommand> fromPathWithSettings = StemmerPatchTrieLoader.loadCompiled(
                    dictionaryFile, true, settings);
            final FrequencyTrie<CompiledPatchCommand> fromPathWithMode = StemmerPatchTrieLoader.loadCompiled(
                    dictionaryFile, true, DEFAULT_REDUCTION_MODE);
            final FrequencyTrie<CompiledPatchCommand> fromStringWithSettings = StemmerPatchTrieLoader.loadCompiled(
                    dictionaryFile.toString(), true, settings);

            assertCompiledTrieSemanticsEqual(expected, fromPathWithSettings, "running", "played", "cities", "run");
            assertCompiledTrieSemanticsEqual(expected, fromPathWithMode, "running", "played", "cities", "run");
            assertCompiledTrieSemanticsEqual(expected, fromStringWithSettings, "running", "played", "cities", "run");
        }

        /**
         * Verifies that metadata-driven loading keeps all configuration dimensions in
         * one explicit object and applies them during compilation.
         *
         * @throws IOException if the test file cannot be written or read
         */
        @Test
        @DisplayName("Metadata overload must drive case and diacritic normalization")
        void shouldLoadUsingExplicitMetadataConfiguration() throws IOException {
            final Path dictionaryFile = writeDictionary("""
                    mÁma	mamA	mámě
                    """);
            final TrieMetadata metadata = TrieMetadata.forCompilation(WordTraversalDirection.BACKWARD,
                    ReductionSettings.withDefaults(DEFAULT_REDUCTION_MODE), DiacriticProcessingMode.REMOVE,
                    CaseProcessingMode.LOWERCASE_WITH_LOCALE_ROOT);

            final FrequencyTrie<String> trie = StemmerPatchTrieLoader.load(dictionaryFile, true, metadata);

            assertAll(() -> assertEquals(DiacriticProcessingMode.REMOVE, trie.metadata().diacriticProcessingMode()),
                    () -> assertEquals(CaseProcessingMode.LOWERCASE_WITH_LOCALE_ROOT,
                            trie.metadata().caseProcessingMode()),
                    () -> assertNotNull(trie.get("MÁMĚ")),
                    () -> assertNotNull(trie.get("mame")));
        }

        /**
         * Verifies that the loader honors {@code storeOriginal=true} by inserting the
         * canonical no-op patch for the stem itself.
         *
         * @throws IOException if the test file cannot be written or read
         */
        @Test
        @DisplayName("storeOriginal=true must make the stem itself resolvable through the no-op patch")
        void shouldStoreOriginalStemWhenRequested() throws IOException {
            final Path dictionaryFile = writeDictionary("""
                    run	running	runs
                    """);

            final FrequencyTrie<String> trie = StemmerPatchTrieLoader.load(dictionaryFile, true,
                    DEFAULT_REDUCTION_MODE);

            final String[] patches = trie.getAll("run");
            final Set<String> reconstructedStems = reconstructAllStemCandidates(trie, "run");

            assertAll(() -> assertNotNull(patches, "Patch array must be returned for stored stem."),
                    () -> assertFalse(reconstructedStems.isEmpty(),
                            "Stored stem must yield at least one reconstructed candidate."),
                    () -> assertEquals(Set.of("run"), reconstructedStems,
                            "Stored stem must reconstruct exactly itself."));
        }

        /**
         * Verifies that the loader honors {@code storeOriginal=false}.
         *
         * @throws IOException if the test file cannot be written or read
         */
        @Test
        @DisplayName("storeOriginal=false must not insert the stem itself unless present as a variant elsewhere")
        void shouldNotStoreOriginalStemWhenDisabled() throws IOException {
            final Path dictionaryFile = writeDictionary("""
                    run	running	runs
                    play	playing	played	plays
                    """);

            final FrequencyTrie<String> trie = StemmerPatchTrieLoader.load(dictionaryFile, false,
                    DEFAULT_REDUCTION_MODE);

            assertNull(trie.get("run"),
                    "Stem itself must not be resolvable when storeOriginal is disabled and the stem is not a variant.");
            assertEquals(Set.of("run"), reconstructAllStemCandidates(trie, "running"),
                    "Variants must still reconstruct the proper stem.");
        }

        /**
         * Verifies that the loader honors forward traversal for right-to-left
         * dictionaries loaded from filesystem overloads.
         *
         * @throws IOException if the test file cannot be written or read
         */
        @Test
        @DisplayName("Explicit right-to-left loading must use forward traversal semantics")
        void shouldUseForwardTraversalForExplicitRightToLeftLoading() throws IOException {
            final Path dictionaryFile = writeDictionary("""
                    كتب	كتابة	كتاب
                    """);

            final ReductionSettings settings = ReductionSettings.withDefaults(DEFAULT_REDUCTION_MODE);
            final FrequencyTrie<String> trie = StemmerPatchTrieLoader.load(dictionaryFile, true, settings,
                    WordTraversalDirection.FORWARD);

            assertEquals(WordTraversalDirection.FORWARD, trie.traversalDirection(),
                    "Right-to-left loading must produce a forward-traversed trie.");
            assertEquals(Set.of("كتب"), reconstructAllStemCandidates(trie, "كتابة"),
                    "Patch reconstruction must use the trie traversal direction.");
        }

        /**
         * Verifies that comment syntax documented by the loader is effectively honored
         * through delegated parsing.
         *
         * @throws IOException if the test file cannot be written or read
         */
        @Test
        @DisplayName("Parser must ignore hash and slash-slash remarks")
        void shouldIgnoreHashAndDoubleSlashRemarks() throws IOException {
            final Path dictionaryFile = writeDictionary("""
                    # full-line hash comment
                    // full-line slash comment
                    run	running	runs // inline slash comment
                    play	playing	played # inline hash comment

                    city	cities
                    """);

            final FrequencyTrie<String> trie = StemmerPatchTrieLoader.load(dictionaryFile, true,
                    DEFAULT_REDUCTION_MODE);

            assertAll(() -> assertEquals(Set.of("run"), reconstructAllStemCandidates(trie, "running")),
                    () -> assertEquals(Set.of("play"), reconstructAllStemCandidates(trie, "played")),
                    () -> assertEquals(Set.of("city"), reconstructAllStemCandidates(trie, "cities")),
                    () -> assertNull(trie.get("#"), "Comment markers must not become dictionary terms."),
                    () -> assertNull(trie.get("//"), "Comment markers must not become dictionary terms."));
        }

        /**
         * Verifies binary save/load round-trip equivalence for the filesystem and
         * stream overloads.
         *
         * @throws IOException if writing or reading fails
         */
        @Test
        @DisplayName("Binary save and load overloads must preserve trie semantics")
        void shouldRoundTripBinaryTrieAcrossAllBinaryOverloads() throws IOException {
            final Path dictionaryFile = writeDictionary("""
                    run	running	runs	runner
                    city	cities
                    study	studies	studying
                    """);
            final Path binaryFile = tempDir.resolve("stemmer-trie.bin.gz");

            final FrequencyTrie<String> original = StemmerPatchTrieLoader.load(dictionaryFile, true,
                    DEFAULT_REDUCTION_MODE);

            StemmerPatchTrieLoader.saveBinary(original, binaryFile);
            final FrequencyTrie<String> fromPath = StemmerPatchTrieLoader.loadBinary(binaryFile);
            final FrequencyTrie<String> fromString = StemmerPatchTrieLoader.loadBinary(binaryFile.toString());
            final FrequencyTrie<CompiledPatchCommand> compiledFromPath = StemmerPatchTrieLoader.loadBinaryCompiled(
                    binaryFile);
            final FrequencyTrie<CompiledPatchCommand> compiledFromString = StemmerPatchTrieLoader.loadBinaryCompiled(
                    binaryFile.toString());

            final byte[] binaryBytes = Files.readAllBytes(binaryFile);
            try (InputStream inputStream = new ByteArrayInputStream(binaryBytes)) {
                final FrequencyTrie<String> fromStream = StemmerPatchTrieLoader.loadBinary(inputStream);
                final FrequencyTrie<CompiledPatchCommand> compiledFromStream;
                try (InputStream compiledInputStream = new ByteArrayInputStream(binaryBytes)) {
                    compiledFromStream = StemmerPatchTrieLoader.loadBinaryCompiled(compiledInputStream);
                }

                assertTriePatchSemanticsEqual(original, fromPath, "run", "running", "runner", "cities", "studying");
                assertTriePatchSemanticsEqual(original, fromString, "run", "running", "runner", "cities", "studying");
                assertTriePatchSemanticsEqual(original, fromStream, "run", "running", "runner", "cities", "studying");
                assertCompiledTrieSemanticsEqual(original, compiledFromPath, "run", "running", "runner", "cities",
                        "studying");
                assertCompiledTrieSemanticsEqual(original, compiledFromString, "run", "running", "runner", "cities",
                        "studying");
                assertCompiledTrieSemanticsEqual(original, compiledFromStream, "run", "running", "runner", "cities",
                        "studying");
            }

            final TrieMetadata metadataFromPath = StemmerPatchTrieLoader.loadBinaryMetadata(binaryFile);
            final TrieMetadata metadataFromString = StemmerPatchTrieLoader.loadBinaryMetadata(binaryFile.toString());
            try (InputStream metadataInputStream = new ByteArrayInputStream(binaryBytes)) {
                final TrieMetadata metadataFromStream = StemmerPatchTrieLoader.loadBinaryMetadata(metadataInputStream);
                assertAll(() -> assertEquals(original.metadata(), metadataFromPath),
                        () -> assertEquals(original.metadata(), metadataFromString),
                        () -> assertEquals(original.metadata(), metadataFromStream));
            }
        }

        /**
         * Verifies that public compiled binary loading materializes final command
         * arrays directly and preserves shared table identity, counts, metadata, and
         * canonical graph size.
         *
         * @throws IOException if writing or reading fails
         */
        @Test
        @DisplayName("Compiled binary loading should materialize one final shared command graph")
        void shouldMaterializeOneFinalSharedCommandGraphFromBinary() throws IOException {
            final PatchCommandEncoder encoder = PatchCommandEncoder.builder().build();
            final String sharedPatch = encoder.encode("ab", "a");
            final String longerDeletionPatch = encoder.encode("xab", "x");
            final FrequencyTrie.Builder<String> builder = new FrequencyTrie.Builder<String>(String[]::new,
                    ReductionSettings
                            .withDefaults(ReductionMode.MERGE_SUBTREES_WITH_EQUIVALENT_RANKED_GET_ALL_RESULTS));
            builder.put("ab", new String(sharedPatch), 2);
            builder.put("xab", longerDeletionPatch);
            builder.put("cb", new String(sharedPatch), 3);
            builder.put("ycb", new String(longerDeletionPatch));
            final FrequencyTrie<String> sourceTrie = builder.build();
            final Path binaryFile = tempDir.resolve("direct-compiled-trie.bin.gz");
            StemmerPatchTrieLoader.saveBinary(sourceTrie, binaryFile);

            final FrequencyTrie<CompiledPatchCommand> compiledTrie = StemmerPatchTrieLoader
                    .loadBinaryCompiled(binaryFile);
            final FrequencyTrie<String> stringTrie = StemmerPatchTrieLoader.loadBinary(binaryFile);
            final CompiledNode<CompiledPatchCommand> suffixBNode = compiledTrie.root().findChild('b');
            final CompiledNode<CompiledPatchCommand> abNode = suffixBNode.findChild('a');
            final CompiledNode<CompiledPatchCommand> cbNode = suffixBNode.findChild('c');

            assertAll(() -> assertEquals(sourceTrie.metadata(), compiledTrie.metadata()),
                    () -> assertEquals(sourceTrie.size(), compiledTrie.size()),
                    () -> assertEquals(sourceTrie.getEntries("ab").get(0).count(),
                            compiledTrie.getEntries("ab").get(0).count()),
                    () -> assertEquals(sourceTrie.getEntries("cb").get(0).count(),
                            compiledTrie.getEntries("cb").get(0).count()),
                    () -> assertEquals(CompiledPatchCommand[].class, abNode.orderedValues().getClass()),
                    () -> assertSame(compiledTrie.get("ab"), compiledTrie.get("cb")),
                    () -> assertSame(abNode.orderedValues()[0], cbNode.orderedValues()[0]),
                    () -> assertEquals("a", compiledTrie.get("ab").apply("ab")),
                    () -> assertEquals("c", compiledTrie.get("cb").apply("cb")),
                    () -> assertEquals("x", compiledTrie.get("xab").apply("xab")),
                    () -> assertEquals("y", compiledTrie.get("ycb").apply("ycb")),
                    () -> assertInstanceOf(String.class, stringTrie.get("ab")),
                    () -> assertInstanceOf(CompiledPatchCommand.class, compiledTrie.get("ab")));
        }

        /**
         * Verifies that binary load overloads with an explicit dense lookup span
         * preserve trie semantics while honoring the dense-layout override.
         */
        @Test
        @DisplayName("Binary dense-span override overloads should load equivalent tries")
        void shouldLoadBinaryWithDenseSpanOverrideOverloads() throws IOException {
            final Path dictionaryFile = writeDictionary("""
                    run	running	runs	runner
                    city	cities
                    study	studies	studying
                    """);
            final Path binaryFile = tempDir.resolve("stemmer-trie-overrides.bin.gz");

            final FrequencyTrie<String> original = StemmerPatchTrieLoader.load(dictionaryFile, true,
                    DEFAULT_REDUCTION_MODE);

            StemmerPatchTrieLoader.saveBinary(original, binaryFile);

            final FrequencyTrie<String> fromPathDefault = StemmerPatchTrieLoader.loadBinary(binaryFile);
            final FrequencyTrie<String> fromPathDefaultByNegative = StemmerPatchTrieLoader.loadBinary(binaryFile,
                    -1);
            final FrequencyTrie<String> fromPathNoDense = StemmerPatchTrieLoader.loadBinary(binaryFile, 0);
            final FrequencyTrie<String> fromStringNoDense = StemmerPatchTrieLoader.loadBinary(binaryFile.toString(), 0);
            final FrequencyTrie<CompiledPatchCommand> compiledDefault = StemmerPatchTrieLoader
                    .loadBinaryCompiled(binaryFile);
            final FrequencyTrie<CompiledPatchCommand> compiledDefaultByNegative = StemmerPatchTrieLoader
                    .loadBinaryCompiled(binaryFile, -1);
            final FrequencyTrie<CompiledPatchCommand> compiledNoDense = StemmerPatchTrieLoader
                    .loadBinaryCompiled(binaryFile, 0);
            final FrequencyTrie<CompiledPatchCommand> compiledStringNoDense = StemmerPatchTrieLoader
                    .loadBinaryCompiled(binaryFile.toString(), 0);

            assertTriePatchSemanticsEqual(original, fromPathDefault, "run", "running", "runner", "cities", "studying");
            assertTriePatchSemanticsEqual(original, fromPathDefaultByNegative, "run", "running", "runner", "cities",
                    "studying");
            assertTriePatchSemanticsEqual(original, fromPathNoDense, "run", "running", "runner", "cities", "studying");
            assertTriePatchSemanticsEqual(original, fromStringNoDense, "run", "running", "runner", "cities",
                    "studying");
            assertCompiledTrieSemanticsEqual(original, compiledDefault, "run", "running", "runner", "cities",
                    "studying");
            assertCompiledTrieSemanticsEqual(original, compiledDefaultByNegative, "run", "running", "runner",
                    "cities", "studying");
            assertCompiledTrieSemanticsEqual(original, compiledNoDense, "run", "running", "runner", "cities",
                    "studying");
            assertCompiledTrieSemanticsEqual(original, compiledStringNoDense, "run", "running", "runner", "cities",
                    "studying");

            assertTrue(compiledDefault.root().hasDenseLookup(),
                    "Default compiled loading should use the documented dense lookup span.");
            assertTrue(compiledDefaultByNegative.root().hasDenseLookup(),
                    "Negative compiled override should use the documented default dense lookup span.");
            assertFalse(fromPathNoDense.root().hasDenseLookup(),
                    "Zero span should disable dense lookup on the loaded root.");
            assertFalse(fromStringNoDense.root().hasDenseLookup(),
                    "Zero span should disable dense lookup on the loaded root.");
            assertFalse(compiledNoDense.root().hasDenseLookup(),
                    "Zero span should disable dense lookup on the directly compiled root.");
            assertFalse(compiledStringNoDense.root().hasDenseLookup(),
                    "String-path zero span should disable dense lookup on the directly compiled root.");
        }

        /**
         * Writes a dictionary file into the temporary directory.
         *
         * @param content dictionary content
         * @return written file path
         * @throws IOException if writing fails
         */
        private Path writeDictionary(final String content) throws IOException {
            final Path file = tempDir.resolve("dictionary-" + System.nanoTime() + ".dict");
            Files.writeString(file, content, StandardCharsets.UTF_8);
            return file;
        }
    }

    /**
     * Bundled dictionary integration tests.
     */
    @Nested
    @Tag("slow")
    @DisplayName("Bundled dictionaries")
    @Tag("compat")
    @Tag("trie")
    @Tag("regression")
    final class BundledDictionaryTests {

        /**
         * Verifies that the current language enumeration exactly matches the bundled
         * language set expected by this project revision.
         */
        @Test
        @DisplayName("Language enum must expose the current bundled language set")
        void shouldExposeCurrentBundledLanguageSet() {
            final Set<StemmerPatchTrieLoader.Language> expectedLanguages = new LinkedHashSet<StemmerPatchTrieLoader.Language>(
                    Arrays.asList(StemmerPatchTrieLoader.Language.CS_CZ, StemmerPatchTrieLoader.Language.DA_DK,
                            StemmerPatchTrieLoader.Language.DE_DE, StemmerPatchTrieLoader.Language.ES_ES,
                            StemmerPatchTrieLoader.Language.FA_IR, StemmerPatchTrieLoader.Language.FI_FI,
                            StemmerPatchTrieLoader.Language.FR_FR, StemmerPatchTrieLoader.Language.HE_IL,
                            StemmerPatchTrieLoader.Language.HU_HU, StemmerPatchTrieLoader.Language.IT_IT,
                            StemmerPatchTrieLoader.Language.NB_NO, StemmerPatchTrieLoader.Language.NL_NL,
                            StemmerPatchTrieLoader.Language.NN_NO, StemmerPatchTrieLoader.Language.PL_PL,
                            StemmerPatchTrieLoader.Language.PT_PT, StemmerPatchTrieLoader.Language.RU_RU,
                            StemmerPatchTrieLoader.Language.SV_SE, StemmerPatchTrieLoader.Language.UK_UA,
                            StemmerPatchTrieLoader.Language.US_UK, StemmerPatchTrieLoader.Language.YI));

            final Set<StemmerPatchTrieLoader.Language> actualLanguages = new LinkedHashSet<StemmerPatchTrieLoader.Language>(
                    Arrays.asList(StemmerPatchTrieLoader.Language.values()));

            assertEquals(expectedLanguages, actualLanguages,
                    "The bundled language enum must match the project's supported language set exactly.");
        }

        /**
         * Verifies that the right-to-left metadata is correctly assigned for the
         * currently supported bundled languages.
         */
        @Test
        @DisplayName("Language enum must mark right-to-left bundled languages correctly")
        void shouldExposeCorrectRightToLeftMetadata() {
            final Set<StemmerPatchTrieLoader.Language> expectedRightToLeftLanguages = Set.of(
                    StemmerPatchTrieLoader.Language.FA_IR, StemmerPatchTrieLoader.Language.HE_IL,
                    StemmerPatchTrieLoader.Language.YI);

            for (StemmerPatchTrieLoader.Language language : StemmerPatchTrieLoader.Language.values()) {
                if (expectedRightToLeftLanguages.contains(language)) {
                    assertTrue(language.isRightToLeft(), () -> language.name() + " must be marked as right-to-left.");
                } else {
                    assertFalse(language.isRightToLeft(),
                            () -> language.name() + " must not be marked as right-to-left.");
                }
            }
        }

        /**
         * Verifies that each registered default-model dictionary compiles into a trie whose
         * {@link FrequencyTrie#getAll(String)} results still reconstruct exactly the
         * same set of stems as the source dictionary.
         *
         * @param scenario      human-readable numbered scenario identifier
         * @param language      tested bundled language
         * @param reductionMode reduction mode
         * @throws IOException if a registered model dictionary cannot be read
         */
        @ParameterizedTest(name = "[{index}] {0}")
        @MethodSource("org.egothor.stemmer.StemmerPatchTrieLoaderTest#bundledDictionaryCases")
        @DisplayName("Bundled dictionaries must preserve all valid stem candidates in getAll()")
        void shouldPreserveAllStemCandidatesForBundledDictionaries(final String scenario,
                final StemmerPatchTrieLoader.Language language, final ReductionMode reductionMode) throws IOException {
            Objects.requireNonNull(scenario, "scenario");
            Objects.requireNonNull(language, "language");
            Objects.requireNonNull(reductionMode, "reductionMode");

            final FrequencyTrie<String> trie = StemmerPatchTrieLoader.load(language, true, reductionMode);
            final Map<String, Set<String>> expectedStemsByWord = readExpectedStems(language);

            assertNotNull(trie, "Compiled trie must be created.");
            assertFalse(expectedStemsByWord.isEmpty(), "Bundled dictionary must not be empty.");
            assertEquals(language.isRightToLeft() ? WordTraversalDirection.FORWARD : WordTraversalDirection.BACKWARD,
                    trie.traversalDirection(), "Trie traversal direction must match language metadata.");

            for (Map.Entry<String, Set<String>> entry : expectedStemsByWord.entrySet()) {
                final String word = entry.getKey();
                final Set<String> expectedStems = entry.getValue();
                final Set<String> actualStems = reconstructAllStemCandidates(trie, word);

                assertFalse(actualStems.isEmpty(),
                        () -> "No patch candidates returned for word '" + word + "' in scenario " + scenario + ".");

                assertEquals(expectedStems, actualStems, () -> "Reconstructed stem candidates differ for word '" + word
                        + "' in scenario " + scenario + "'. Expected: " + expectedStems + ", actual: " + actualStems);
            }
        }

        /**
         * Verifies that representative registered model dictionaries load equivalently through
         * both reduction-setting and reduction-mode overloads.
         *
         * @param scenario scenario identifier
         * @param language tested language
         * @throws IOException if reading fails
         */
        @ParameterizedTest(name = "[{index}] {0}")
        @MethodSource("org.egothor.stemmer.StemmerPatchTrieLoaderTest#bundledLanguageSamples")
        @DisplayName("Bundled dictionary overloads must produce equivalent trie semantics")
        void shouldLoadBundledDictionariesEquivalentlyAcrossOverloads(final String scenario,
                final StemmerPatchTrieLoader.Language language) throws IOException {
            final ReductionSettings settings = ReductionSettings.withDefaults(DEFAULT_REDUCTION_MODE);

            final FrequencyTrie<String> viaSettings = StemmerPatchTrieLoader.load(language, true, settings);
            final FrequencyTrie<String> viaMode = StemmerPatchTrieLoader.load(language, true, DEFAULT_REDUCTION_MODE);

            final Map<String, Set<String>> expectedStemsByWord = readExpectedStems(language);
            int counter = 0;

            for (Map.Entry<String, Set<String>> entry : expectedStemsByWord.entrySet()) {
                assertTriePatchSemanticsEqual(viaSettings, viaMode, entry.getKey());
                counter++;
                if (counter >= REPRESENTATIVE_BUNDLED_WORD_COUNT) {
                    break;
                }
            }

            assertFalse(expectedStemsByWord.isEmpty(),
                    "Scenario " + scenario + " must provide at least one registered model dictionary entry.");
        }
    }

    /**
     * Reads the registered model dictionary and builds a mapping of surface word to all
     * stems it is associated with in the source data.
     *
     * <p>
     * The method intentionally delegates parsing to {@link StemmerDictionaryParser}
     * so that expected values follow the same comment and normalization rules as
     * the production loader.
     * </p>
     *
     * @param language bundled language
     * @return expected stems by surface word
     * @throws IOException if the bundled resource cannot be read
     */
    private static Map<String, Set<String>> readExpectedStems(final StemmerPatchTrieLoader.Language language)
            throws IOException {
        final Map<String, Set<String>> expectedStemsByWord = new LinkedHashMap<String, Set<String>>();
        final String resourcePath = language.resourcePath();

        try (InputStream inputStream = openBundledResource(resourcePath);
                BufferedReader reader = new BufferedReader(
                        new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {

            StemmerDictionaryParser.parse(reader, resourcePath, (stem, variants, lineNumber) -> {
                registerExpectedStem(expectedStemsByWord, stem, stem);

                for (String variant : variants) {
                    registerExpectedStem(expectedStemsByWord, variant, stem);
                }
            });
        }

        return expectedStemsByWord;
    }

    /**
     * Registers one expected stem for one surface word.
     *
     * @param expectedStemsByWord expected stem mapping
     * @param word                surface word
     * @param stem                expected stem
     */
    private static void registerExpectedStem(final Map<String, Set<String>> expectedStemsByWord, final String word,
            final String stem) {
        Set<String> stems = expectedStemsByWord.get(word);
        if (stems == null) {
            stems = new LinkedHashSet<String>();
            expectedStemsByWord.put(word, stems);
        }
        stems.add(stem);
    }

    /**
     * Reconstructs all stem candidates for the supplied word from all patch
     * commands returned by {@link FrequencyTrie#getAll(String)}.
     *
     * @param trie compiled patch trie
     * @param word surface word
     * @return reconstructed stem candidates
     */
    private static Set<String> reconstructAllStemCandidates(final FrequencyTrie<String> trie, final String word) {
        final String[] patchCommands = trie.getAll(word);
        final Set<String> stems = new LinkedHashSet<String>();

        if (patchCommands == null) {
            return stems;
        }

        for (String patchCommand : patchCommands) {
            stems.add(PatchCommandEncoder.apply(word, patchCommand, trie.traversalDirection()));
        }

        return stems;
    }

    /**
     * Reconstructs all stem candidates for the supplied word from compiled patch
     * commands returned by {@link FrequencyTrie#getAll(String)}.
     *
     * @param trie compiled patch-command trie
     * @param word surface word
     * @return reconstructed stem candidates
     */
    private static Set<String> reconstructAllCompiledStemCandidates(final FrequencyTrie<CompiledPatchCommand> trie,
            final String word) {
        final CompiledPatchCommand[] patchCommands = trie.getAll(word);
        final Set<String> stems = new LinkedHashSet<String>();

        if (patchCommands == null) {
            return stems;
        }

        for (CompiledPatchCommand patchCommand : patchCommands) {
            stems.add(patchCommand.apply(word));
        }

        return stems;
    }

    /**
     * Creates a compiled trie whose identity distinguishes the direct BinaryIO
     * result from any graph produced through deprecated String loading.
     *
     * @return compiled trie sentinel
     */
    private static FrequencyTrie<CompiledPatchCommand> compiledRoutingSentinel() {
        final String serializedPatch = PatchCommandEncoder.builder().build().encode("running", "run");
        final CompiledPatchCommand compiledPatch = CompiledPatchCommand.compile(serializedPatch,
                WordTraversalDirection.BACKWARD);
        return new FrequencyTrie.Builder<CompiledPatchCommand>(CompiledPatchCommand[]::new,
                ReductionSettings.withDefaults(DEFAULT_REDUCTION_MODE))
                        .put("running", compiledPatch)
                        .build();
    }

    /**
     * Creates a valid String-valued trie that allows the deprecated two-stage route
     * to execute rather than failing because of incomplete mock setup.
     *
     * @return valid String-valued fallback trie
     */
    private static FrequencyTrie<String> stringRoutingFallback() {
        final String serializedPatch = PatchCommandEncoder.builder().build().encode("running", "run");
        return new FrequencyTrie.Builder<String>(String[]::new,
                ReductionSettings.withDefaults(DEFAULT_REDUCTION_MODE))
                        .put("running", serializedPatch)
                        .build();
    }

    /**
     * Verifies semantic equality of two tries for the supplied words by comparing
     * both their raw patch arrays and reconstructed stem sets.
     *
     * @param expected reference trie
     * @param actual   compared trie
     * @param words    words to verify
     */
    private static void assertTriePatchSemanticsEqual(final FrequencyTrie<String> expected,
            final FrequencyTrie<String> actual, final String... words) {
        for (String word : words) {
            assertAll(
                    () -> assertArrayEquals(expected.getAll(word), actual.getAll(word),
                            "Patch arrays must match for word '" + word + "'."),
                    () -> assertEquals(reconstructAllStemCandidates(expected, word),
                            reconstructAllStemCandidates(actual, word),
                            "Reconstructed stems must match for word '" + word + "'."));
        }
    }

    /**
     * Verifies semantic equality of a textual patch trie and a compiled patch trie
     * for the supplied words.
     *
     * @param expected reference trie with textual patch commands
     * @param actual   compared trie with compiled patch commands
     * @param words    words to verify
     */
    private static void assertCompiledTrieSemanticsEqual(final FrequencyTrie<String> expected,
            final FrequencyTrie<CompiledPatchCommand> actual, final String... words) {
        assertAll(() -> assertEquals(expected.metadata(), actual.metadata(), "Trie metadata must be preserved."),
                () -> assertEquals(expected.traversalDirection(), actual.traversalDirection(),
                        "Trie traversal direction must be preserved."));

        for (String word : words) {
            assertEquals(reconstructAllStemCandidates(expected, word), reconstructAllCompiledStemCandidates(actual, word),
                    "Compiled patch stems must match textual patch stems for word '" + word + "'.");
        }
    }

    /**
     * Opens one registered model dictionary resource.
     *
     * @param resourcePath classpath resource path
     * @return opened input stream
     * @throws IOException if the resource cannot be found
     */
    private static InputStream openBundledResource(final String resourcePath) throws IOException {
        final InputStream inputStream = StemmerPatchTrieLoaderTest.class.getClassLoader()
                .getResourceAsStream(resourcePath);
        if (inputStream == null) {
            throw new IOException("Bundled stemmer resource not found: " + resourcePath);
        }
        return new GZIPInputStream(inputStream);
    }

    /**
     * Minimal checked-exception-friendly operation used by null-contract tests.
     */
    @FunctionalInterface
    private interface ExecutableOperation {

        /**
         * Executes the operation.
         *
         * @throws Exception if execution fails
         */
        void execute() throws Exception;
    }
}
