package click.kamil.springstatemachineexporter.analysis.service;

import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
import click.kamil.springstatemachineexporter.analysis.resolver.ProjectModuleGraph;
import click.kamil.springstatemachineexporter.analysis.resolver.SiblingDependencyResolver;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;

import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Collections;

import static org.assertj.core.api.Assertions.assertThat;

/**
 * Path binding expansion must resolve helpers declared in sibling modules without executing the build.
 */
class MultiModulePathBindingExpanderTest {

    @Test
    void shouldExpandEventUsingHelperFromSiblingModule(@TempDir Path tempDir) throws Exception {
        Path root = tempDir.resolve("order-system");
        Files.createDirectories(root);
        Files.writeString(root.resolve("settings.gradle"), "include 'api-module', 'core-module'");

        Path apiModule = root.resolve("api-module");
        Files.createDirectories(apiModule.resolve("src/main/java/com/example/util"));
        Files.writeString(apiModule.resolve("build.gradle"), "");
        Files.writeString(apiModule.resolve("src/main/java/com/example/util/EventNormalizer.java"), """
                package com.example.util;
                public class EventNormalizer {
                    public static String normalize(String raw) {
                        return raw.toUpperCase();
                    }
                }
                """);

        Path coreModule = root.resolve("core-module");
        Files.createDirectories(coreModule.resolve("src/main/java/com/example"));
        Files.writeString(coreModule.resolve("build.gradle"),
                "dependencies { implementation project(':api-module') }");
        Files.writeString(coreModule.resolve("src/main/java/com/example/OrderGateway.java"), """
                package com.example;
                import com.example.util.EventNormalizer;
                public class OrderController {
                    OrderGateway gateway;
                    public void transition(String event) {
                        gateway.trigger(event);
                    }
                }
                class OrderGateway {
                    void trigger(String event) {
                        OrderEvent.valueOf(EventNormalizer.normalize(event));
                    }
                }
                enum OrderEvent { PAY, SHIP }
                """);

        SiblingDependencyResolver siblingResolver = new SiblingDependencyResolver();
        ProjectModuleGraph graph = siblingResolver.analyzeProject(coreModule);
        Set<Path> scanPaths = graph.resolveScanPaths(coreModule, false);

        CodebaseContext context = new CodebaseContext();
        context.setResolveBindings(true);
        context.scan(scanPaths, Collections.emptySet());

        assertThat(context.getTypeDeclaration("com.example.util.EventNormalizer")).isNotNull();

        HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
        Map<String, List<click.kamil.springstatemachineexporter.analysis.model.CallEdge>> callGraph =
                engine.buildCallGraph();

        EntryPoint entryPoint = EntryPoint.builder()
                .className("com.example.OrderController")
                .methodName("transition")
                .name("POST /api/order/{event}")
                .parameters(List.of(EntryPoint.Parameter.builder()
                        .name("event")
                        .type("String")
                        .annotations(List.of("PathVariable"))
                        .build()))
                .build();

        List<Map<String, String>> variants =
                EntryPointBindingExpander.expandPathVariableBindings(entryPoint, context, callGraph);

        assertThat(variants).extracting(map -> map.get("event"))
                .containsExactlyInAnyOrder("PAY", "SHIP");
        assertThat(EntryPointBindingExpander.withResolvedPath(
                entryPoint, Map.of("event", "PAY")).getName())
                .isEqualTo("POST /api/order/PAY");
    }

    @Test
    void shouldExpandEventUsingHelperFromSiblingMavenModule(@TempDir Path tempDir) throws Exception {
        Path root = tempDir.resolve("order-maven");
        Files.createDirectories(root.resolve(".git"));
        Files.writeString(root.resolve("pom.xml"), """
                <project><groupId>com.example</groupId><artifactId>order-maven</artifactId></project>
                """);

        Path apiModule = root.resolve("api-module");
        Files.createDirectories(apiModule.resolve("src/main/java/com/example/util"));
        Files.writeString(apiModule.resolve("pom.xml"), """
                <project><groupId>com.example</groupId><artifactId>api-module</artifactId></project>
                """);
        Files.writeString(apiModule.resolve("src/main/java/com/example/util/EventNormalizer.java"), """
                package com.example.util;
                public class EventNormalizer {
                    public static String normalize(String raw) {
                        return raw.toUpperCase();
                    }
                }
                """);

        Path coreModule = root.resolve("core-module");
        Files.createDirectories(coreModule.resolve("src/main/java/com/example"));
        Files.writeString(coreModule.resolve("pom.xml"), """
                <project>
                  <groupId>com.example</groupId>
                  <artifactId>core-module</artifactId>
                  <dependencies>
                    <dependency>
                      <groupId>com.example</groupId>
                      <artifactId>api-module</artifactId>
                      <version>1.0</version>
                    </dependency>
                  </dependencies>
                </project>
                """);
        Files.writeString(coreModule.resolve("src/main/java/com/example/OrderGateway.java"), """
                package com.example;
                import com.example.util.EventNormalizer;
                public class OrderController {
                    OrderGateway gateway;
                    public void transition(String event) {
                        gateway.trigger(event);
                    }
                }
                class OrderGateway {
                    void trigger(String event) {
                        OrderEvent.valueOf(EventNormalizer.normalize(event));
                    }
                }
                enum OrderEvent { PAY, SHIP }
                """);

        SiblingDependencyResolver siblingResolver = new SiblingDependencyResolver();
        ProjectModuleGraph graph = siblingResolver.analyzeProject(coreModule);
        Set<Path> scanPaths = graph.resolveScanPaths(coreModule, false);

        CodebaseContext context = new CodebaseContext();
        context.setResolveBindings(true);
        context.scan(scanPaths, Collections.emptySet());

        assertThat(context.getTypeDeclaration("com.example.util.EventNormalizer")).isNotNull();

        HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
        Map<String, List<click.kamil.springstatemachineexporter.analysis.model.CallEdge>> callGraph =
                engine.buildCallGraph();

        EntryPoint entryPoint = EntryPoint.builder()
                .className("com.example.OrderController")
                .methodName("transition")
                .name("POST /api/order/{event}")
                .parameters(List.of(EntryPoint.Parameter.builder()
                        .name("event")
                        .type("String")
                        .annotations(List.of("PathVariable"))
                        .build()))
                .build();

        List<Map<String, String>> variants =
                EntryPointBindingExpander.expandPathVariableBindings(entryPoint, context, callGraph);

        assertThat(variants).extracting(map -> map.get("event"))
                .containsExactlyInAnyOrder("PAY", "SHIP");
    }

    @Test
    void shouldExpandMachineTypeFromIfEqualsOnSiblingModuleHelper(@TempDir Path tempDir) throws Exception {
        Path root = tempDir.resolve("enterprise-style");
        Files.createDirectories(root);
        Files.writeString(root.resolve("settings.gradle"), "include 'api-module', 'core-module'");

        Path apiModule = root.resolve("api-module");
        Files.createDirectories(apiModule.resolve("src/main/java/com/example/util"));
        Files.writeString(apiModule.resolve("build.gradle"), "");
        Files.writeString(apiModule.resolve("src/main/java/com/example/util/MachineTypeNormalizer.java"), """
                package com.example.util;
                public class MachineTypeNormalizer {
                    public static String normalize(String raw) {
                        return raw.trim();
                    }
                }
                """);

        Path coreModule = root.resolve("core-module");
        Files.createDirectories(coreModule.resolve("src/main/java/com/example"));
        Files.writeString(coreModule.resolve("build.gradle"),
                "dependencies { implementation project(':api-module') }");
        Files.writeString(coreModule.resolve("src/main/java/com/example/MachineController.java"), """
                package com.example;
                import com.example.util.MachineTypeNormalizer;
                public class MachineController {
                    StateMachineDispatcher dispatcher;
                    public void transition(String machineType, String event) {
                        dispatcher.dispatch(machineType, event);
                    }
                }
                class StateMachineDispatcher {
                    void dispatch(String machineType, String eventString) {
                        if ("ORDER".equalsIgnoreCase(MachineTypeNormalizer.normalize(machineType))) {
                            OrderEvent.valueOf(eventString.toUpperCase());
                        } else if ("DOCUMENT".equalsIgnoreCase(MachineTypeNormalizer.normalize(machineType))) {
                            DocumentEvent.valueOf(eventString.toUpperCase());
                        }
                    }
                }
                enum OrderEvent { PAY, SHIP }
                enum DocumentEvent { SUBMIT, APPROVE }
                """);

        SiblingDependencyResolver siblingResolver = new SiblingDependencyResolver();
        ProjectModuleGraph graph = siblingResolver.analyzeProject(coreModule);
        Set<Path> scanPaths = graph.resolveScanPaths(coreModule, false);

        CodebaseContext context = new CodebaseContext();
        context.setResolveBindings(true);
        context.scan(scanPaths, Collections.emptySet());

        HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
        Map<String, List<click.kamil.springstatemachineexporter.analysis.model.CallEdge>> callGraph =
                engine.buildCallGraph();

        EntryPoint entryPoint = EntryPoint.builder()
                .className("com.example.MachineController")
                .methodName("transition")
                .name("POST /api/machine/{machineType}/transition/{event}")
                .parameters(List.of(
                        EntryPoint.Parameter.builder()
                                .name("machineType")
                                .type("String")
                                .annotations(List.of("PathVariable"))
                                .build(),
                        EntryPoint.Parameter.builder()
                                .name("event")
                                .type("String")
                                .annotations(List.of("PathVariable"))
                                .build()))
                .build();

        List<Map<String, String>> variants =
                EntryPointBindingExpander.expandPathVariableBindings(entryPoint, context, callGraph);

        assertThat(variants.stream().map(v -> v.get("machineType")).distinct())
                .containsExactlyInAnyOrder("ORDER", "DOCUMENT");
        assertThat(variants.stream()
                .filter(v -> "ORDER".equals(v.get("machineType")))
                .map(v -> v.get("event"))
                .toList())
                .containsExactlyInAnyOrder("PAY", "SHIP");
    }

    @Test
    void shouldExpandMachineTypeFromBooleanPredicateInSiblingModule(@TempDir Path tempDir) throws Exception {
        Path root = tempDir.resolve("enterprise-style");
        Files.createDirectories(root);
        Files.writeString(root.resolve("settings.gradle"), "include 'api-module', 'core-module'");

        Path apiModule = root.resolve("api-module");
        Files.createDirectories(apiModule.resolve("src/main/java/com/example/util"));
        Files.writeString(apiModule.resolve("build.gradle"), "");
        Files.writeString(apiModule.resolve("src/main/java/com/example/util/MachineTypeMatcher.java"), """
                package com.example.util;
                public class MachineTypeMatcher {
                    public static boolean matchesOrder(String raw) {
                        return "ORDER".equalsIgnoreCase(raw.trim());
                    }
                    public static boolean matchesDocument(String raw) {
                        return "DOCUMENT".equalsIgnoreCase(raw.trim());
                    }
                }
                """);

        Path coreModule = root.resolve("core-module");
        Files.createDirectories(coreModule.resolve("src/main/java/com/example"));
        Files.writeString(coreModule.resolve("build.gradle"),
                "dependencies { implementation project(':api-module') }");
        Files.writeString(coreModule.resolve("src/main/java/com/example/MachineController.java"), """
                package com.example;
                import com.example.util.MachineTypeMatcher;
                public class MachineController {
                    StateMachineDispatcher dispatcher;
                    public void transition(String machineType, String event) {
                        dispatcher.dispatch(machineType, event);
                    }
                }
                class StateMachineDispatcher {
                    void dispatch(String machineType, String eventString) {
                        if (MachineTypeMatcher.matchesOrder(machineType)) {
                            OrderEvent.valueOf(eventString.toUpperCase());
                        } else if (MachineTypeMatcher.matchesDocument(machineType)) {
                            DocumentEvent.valueOf(eventString.toUpperCase());
                        }
                    }
                }
                enum OrderEvent { PAY, SHIP }
                enum DocumentEvent { SUBMIT, APPROVE }
                """);

        SiblingDependencyResolver siblingResolver = new SiblingDependencyResolver();
        ProjectModuleGraph graph = siblingResolver.analyzeProject(coreModule);
        Set<Path> scanPaths = graph.resolveScanPaths(coreModule, false);

        CodebaseContext context = new CodebaseContext();
        context.setResolveBindings(true);
        context.scan(scanPaths, Collections.emptySet());

        HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
        Map<String, List<click.kamil.springstatemachineexporter.analysis.model.CallEdge>> callGraph =
                engine.buildCallGraph();

        EntryPoint entryPoint = EntryPoint.builder()
                .className("com.example.MachineController")
                .methodName("transition")
                .name("POST /api/machine/{machineType}/transition/{event}")
                .parameters(List.of(
                        EntryPoint.Parameter.builder()
                                .name("machineType")
                                .type("String")
                                .annotations(List.of("PathVariable"))
                                .build(),
                        EntryPoint.Parameter.builder()
                                .name("event")
                                .type("String")
                                .annotations(List.of("PathVariable"))
                                .build()))
                .build();

        List<Map<String, String>> variants =
                EntryPointBindingExpander.expandPathVariableBindings(entryPoint, context, callGraph);

        assertThat(variants.stream().map(v -> v.get("machineType")).distinct())
                .containsExactlyInAnyOrder("ORDER", "DOCUMENT");
    }

    @Test
    void shouldExpandCommandKeyFromCrossClassStaticMapInSiblingModule(@TempDir Path tempDir) throws Exception {
        Path root = tempDir.resolve("order-system");
        Files.createDirectories(root);
        Files.writeString(root.resolve("settings.gradle"), "include 'api-module', 'core-module'");

        Path apiModule = root.resolve("api-module");
        Files.createDirectories(apiModule.resolve("src/main/java/com/example/routes"));
        Files.writeString(apiModule.resolve("build.gradle"), "");
        Files.writeString(apiModule.resolve("src/main/java/com/example/routes/CommandRoutes.java"), """
                package com.example.routes;
                import java.util.Map;
                public class CommandRoutes {
                    public static final Map<String, String> ROUTES = Map.of(
                            "order.pay", "OrderEvent.PAY",
                            "order.ship", "OrderEvent.SHIP");
                }
                """);

        Path coreModule = root.resolve("core-module");
        Files.createDirectories(coreModule.resolve("src/main/java/com/example"));
        Files.writeString(coreModule.resolve("build.gradle"),
                "dependencies { implementation project(':api-module') }");
        Files.writeString(coreModule.resolve("src/main/java/com/example/GenericCommandController.java"), """
                package com.example;
                import com.example.routes.CommandRoutes;
                public class GenericCommandController {
                    OrderGateway gateway;
                    public void execute(String commandKey) {
                        gateway.trigger(commandKey);
                    }
                }
                class OrderGateway {
                    void trigger(String commandKey) {
                        OrderEvent event = OrderEvent.valueOf(CommandRoutes.ROUTES.get(commandKey));
                        StateMachine sm = new StateMachine();
                        sm.sendEvent(event);
                    }
                }
                enum OrderEvent { PAY, SHIP }
                class StateMachine { void sendEvent(OrderEvent event) {} }
                """);

        SiblingDependencyResolver siblingResolver = new SiblingDependencyResolver();
        ProjectModuleGraph graph = siblingResolver.analyzeProject(coreModule);
        Set<Path> scanPaths = graph.resolveScanPaths(coreModule, false);

        CodebaseContext context = new CodebaseContext();
        context.setResolveBindings(true);
        context.scan(scanPaths, Collections.emptySet());

        HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
        Map<String, List<click.kamil.springstatemachineexporter.analysis.model.CallEdge>> callGraph =
                engine.buildCallGraph();

        EntryPoint entryPoint = EntryPoint.builder()
                .className("com.example.GenericCommandController")
                .methodName("execute")
                .name("POST /api/commands/{commandKey}")
                .parameters(List.of(EntryPoint.Parameter.builder()
                        .name("commandKey")
                        .type("String")
                        .annotations(List.of("PathVariable"))
                        .build()))
                .build();

        List<Map<String, String>> variants =
                EntryPointBindingExpander.expandPathVariableBindings(entryPoint, context, callGraph);

        assertThat(variants).extracting(map -> map.get("commandKey"))
                .containsExactlyInAnyOrder("order.pay", "order.ship");
    }

    @Test
    void shouldExpandMachineTypeFromCrossClassConstantInSiblingModuleIfEquals(@TempDir Path tempDir) throws Exception {
        Path root = tempDir.resolve("enterprise-style");
        Files.createDirectories(root);
        Files.writeString(root.resolve("settings.gradle"), "include 'api-module', 'core-module'");

        Path apiModule = root.resolve("api-module");
        Files.createDirectories(apiModule.resolve("src/main/java/com/example/constants"));
        Files.writeString(apiModule.resolve("build.gradle"), "");
        Files.writeString(apiModule.resolve("src/main/java/com/example/constants/MachineTypes.java"), """
                package com.example.constants;
                public class MachineTypes {
                    public static final String ORDER = "ORDER";
                    public static final String DOCUMENT = "DOCUMENT";
                }
                """);

        Path coreModule = root.resolve("core-module");
        Files.createDirectories(coreModule.resolve("src/main/java/com/example"));
        Files.writeString(coreModule.resolve("build.gradle"),
                "dependencies { implementation project(':api-module') }");
        Files.writeString(coreModule.resolve("src/main/java/com/example/MachineController.java"), """
                package com.example;
                import com.example.constants.MachineTypes;
                public class MachineController {
                    StateMachineDispatcher dispatcher;
                    public void transition(String machineType, String event) {
                        dispatcher.dispatch(machineType, event);
                    }
                }
                class StateMachineDispatcher {
                    void dispatch(String machineType, String eventString) {
                        if (MachineTypes.ORDER.equalsIgnoreCase(machineType)) {
                            OrderEvent.valueOf(eventString.toUpperCase());
                        } else if (MachineTypes.DOCUMENT.equalsIgnoreCase(machineType)) {
                            DocumentEvent.valueOf(eventString.toUpperCase());
                        }
                    }
                }
                enum OrderEvent { PAY, SHIP }
                enum DocumentEvent { SUBMIT, APPROVE }
                """);

        SiblingDependencyResolver siblingResolver = new SiblingDependencyResolver();
        ProjectModuleGraph graph = siblingResolver.analyzeProject(coreModule);
        Set<Path> scanPaths = graph.resolveScanPaths(coreModule, false);

        CodebaseContext context = new CodebaseContext();
        context.setResolveBindings(true);
        context.scan(scanPaths, Collections.emptySet());

        HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
        Map<String, List<click.kamil.springstatemachineexporter.analysis.model.CallEdge>> callGraph =
                engine.buildCallGraph();

        EntryPoint entryPoint = EntryPoint.builder()
                .className("com.example.MachineController")
                .methodName("transition")
                .name("POST /api/machine/{machineType}/transition/{event}")
                .parameters(List.of(
                        EntryPoint.Parameter.builder()
                                .name("machineType")
                                .type("String")
                                .annotations(List.of("PathVariable"))
                                .build(),
                        EntryPoint.Parameter.builder()
                                .name("event")
                                .type("String")
                                .annotations(List.of("PathVariable"))
                                .build()))
                .build();

        List<Map<String, String>> variants =
                EntryPointBindingExpander.expandPathVariableBindings(entryPoint, context, callGraph);

        assertThat(variants.stream().map(v -> v.get("machineType")).distinct())
                .containsExactlyInAnyOrder("ORDER", "DOCUMENT");
    }

    @Test
    void shouldExpandCommandKeyFromSwitchExpressionWithSiblingModuleCaseConstants(@TempDir Path tempDir) throws Exception {
        Path root = tempDir.resolve("order-system");
        Files.createDirectories(root);
        Files.writeString(root.resolve("settings.gradle"), "include 'api-module', 'core-module'");

        Path apiModule = root.resolve("api-module");
        Files.createDirectories(apiModule.resolve("src/main/java/com/example/constants"));
        Files.writeString(apiModule.resolve("build.gradle"), "");
        Files.writeString(apiModule.resolve("src/main/java/com/example/constants/CommandKeys.java"), """
                package com.example.constants;
                public class CommandKeys {
                    public static final String PAY_KEY = "order.pay";
                    public static final String SHIP_KEY = "order.ship";
                }
                """);

        Path coreModule = root.resolve("core-module");
        Files.createDirectories(coreModule.resolve("src/main/java/com/example"));
        Files.writeString(coreModule.resolve("build.gradle"),
                "dependencies { implementation project(':api-module') }");
        Files.writeString(coreModule.resolve("src/main/java/com/example/GenericCommandController.java"), """
                package com.example;
                import com.example.constants.CommandKeys;
                public class GenericCommandController {
                    OrderGateway gateway;
                    public void execute(String commandKey) {
                        gateway.trigger(commandKey);
                    }
                }
                class OrderGateway {
                    void trigger(String commandKey) {
                        DomainCommand command = switch (commandKey) {
                            case CommandKeys.PAY_KEY -> DomainCommand.ORDER_PAY;
                            case CommandKeys.SHIP_KEY -> DomainCommand.ORDER_SHIP;
                            default -> throw new IllegalArgumentException("Unknown command: " + commandKey);
                        };
                        StateMachine sm = new StateMachine();
                        sm.sendEvent(command);
                    }
                }
                enum DomainCommand { ORDER_PAY, ORDER_SHIP }
                class StateMachine { void sendEvent(DomainCommand command) {} }
                """);

        SiblingDependencyResolver siblingResolver = new SiblingDependencyResolver();
        ProjectModuleGraph graph = siblingResolver.analyzeProject(coreModule);
        Set<Path> scanPaths = graph.resolveScanPaths(coreModule, false);

        CodebaseContext context = new CodebaseContext();
        context.setResolveBindings(true);
        context.scan(scanPaths, Collections.emptySet());

        HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
        Map<String, List<click.kamil.springstatemachineexporter.analysis.model.CallEdge>> callGraph =
                engine.buildCallGraph();

        EntryPoint entryPoint = EntryPoint.builder()
                .className("com.example.GenericCommandController")
                .methodName("execute")
                .name("POST /api/commands/{commandKey}")
                .parameters(List.of(EntryPoint.Parameter.builder()
                        .name("commandKey")
                        .type("String")
                        .annotations(List.of("PathVariable"))
                        .build()))
                .build();

        List<Map<String, String>> variants =
                EntryPointBindingExpander.expandPathVariableBindings(entryPoint, context, callGraph);

        assertThat(variants).extracting(map -> map.get("commandKey"))
                .containsExactlyInAnyOrder("order.pay", "order.ship");
    }

    @Test
    void shouldExpandCommandKeyFromSiblingModuleConstantMapKeys(@TempDir Path tempDir) throws Exception {
        Path root = tempDir.resolve("order-system");
        Files.createDirectories(root);
        Files.writeString(root.resolve("settings.gradle"), "include 'api-module', 'core-module'");

        Path apiModule = root.resolve("api-module");
        Files.createDirectories(apiModule.resolve("src/main/java/com/example/constants"));
        Files.writeString(apiModule.resolve("build.gradle"), "");
        Files.writeString(apiModule.resolve("src/main/java/com/example/constants/CommandKeys.java"), """
                package com.example.constants;
                public class CommandKeys {
                    public static final String PAY_KEY = "order.pay";
                    public static final String SHIP_KEY = "order.ship";
                }
                """);

        Path coreModule = root.resolve("core-module");
        Files.createDirectories(coreModule.resolve("src/main/java/com/example"));
        Files.writeString(coreModule.resolve("build.gradle"),
                "dependencies { implementation project(':api-module') }");
        Files.writeString(coreModule.resolve("src/main/java/com/example/GenericCommandController.java"), """
                package com.example;
                import com.example.constants.CommandKeys;
                import java.util.Map;
                public class GenericCommandController {
                    OrderGateway gateway;
                    public void execute(String commandKey) {
                        gateway.trigger(commandKey);
                    }
                }
                class OrderGateway {
                    private static final Map<String, OrderEvent> ROUTES = Map.of(
                            CommandKeys.PAY_KEY, OrderEvent.PAY,
                            CommandKeys.SHIP_KEY, OrderEvent.SHIP);
                    void trigger(String commandKey) {
                        StateMachine sm = new StateMachine();
                        sm.sendEvent(ROUTES.get(commandKey));
                    }
                }
                enum OrderEvent { PAY, SHIP }
                class StateMachine { void sendEvent(OrderEvent event) {} }
                """);

        SiblingDependencyResolver siblingResolver = new SiblingDependencyResolver();
        ProjectModuleGraph graph = siblingResolver.analyzeProject(coreModule);
        Set<Path> scanPaths = graph.resolveScanPaths(coreModule, false);

        CodebaseContext context = new CodebaseContext();
        context.setResolveBindings(true);
        context.scan(scanPaths, Collections.emptySet());

        HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
        Map<String, List<click.kamil.springstatemachineexporter.analysis.model.CallEdge>> callGraph =
                engine.buildCallGraph();

        EntryPoint entryPoint = EntryPoint.builder()
                .className("com.example.GenericCommandController")
                .methodName("execute")
                .name("POST /api/commands/{commandKey}")
                .parameters(List.of(EntryPoint.Parameter.builder()
                        .name("commandKey")
                        .type("String")
                        .annotations(List.of("PathVariable"))
                        .build()))
                .build();

        List<Map<String, String>> variants =
                EntryPointBindingExpander.expandPathVariableBindings(entryPoint, context, callGraph);

        assertThat(variants).extracting(map -> map.get("commandKey"))
                .containsExactlyInAnyOrder("order.pay", "order.ship");
    }

    @Test
    void shouldExpandCommandKeyFromStaticBlockPutAllInSiblingModule(@TempDir Path tempDir) throws Exception {
        Path root = tempDir.resolve("order-system");
        Files.createDirectories(root);
        Files.writeString(root.resolve("settings.gradle"), "include 'api-module', 'core-module'");

        Path apiModule = root.resolve("api-module");
        Files.createDirectories(apiModule.resolve("src/main/java/com/example/routes"));
        Files.writeString(apiModule.resolve("build.gradle"), "");
        Files.writeString(apiModule.resolve("src/main/java/com/example/routes/CommandRoutes.java"), """
                package com.example.routes;
                import java.util.Map;
                public class CommandRoutes {
                    public static final Map<String, String> SEED = Map.of(
                            "order.pay", "OrderEvent.PAY",
                            "order.ship", "OrderEvent.SHIP");
                }
                """);

        Path coreModule = root.resolve("core-module");
        Files.createDirectories(coreModule.resolve("src/main/java/com/example"));
        Files.writeString(coreModule.resolve("build.gradle"),
                "dependencies { implementation project(':api-module') }");
        Files.writeString(coreModule.resolve("src/main/java/com/example/GenericCommandController.java"), """
                package com.example;
                import com.example.routes.CommandRoutes;
                import java.util.HashMap;
                import java.util.Map;
                public class GenericCommandController {
                    OrderGateway gateway;
                    public void execute(String commandKey) {
                        gateway.trigger(commandKey);
                    }
                }
                class OrderGateway {
                    private static final Map<String, OrderEvent> ROUTES;
                    static {
                        ROUTES = new HashMap<>();
                        ROUTES.putAll(CommandRoutes.SEED);
                    }
                    void trigger(String commandKey) {
                        OrderEvent event = OrderEvent.valueOf(ROUTES.get(commandKey));
                        StateMachine sm = new StateMachine();
                        sm.sendEvent(event);
                    }
                }
                enum OrderEvent { PAY, SHIP }
                class StateMachine { void sendEvent(OrderEvent event) {} }
                """);

        SiblingDependencyResolver siblingResolver = new SiblingDependencyResolver();
        ProjectModuleGraph graph = siblingResolver.analyzeProject(coreModule);
        Set<Path> scanPaths = graph.resolveScanPaths(coreModule, false);

        CodebaseContext context = new CodebaseContext();
        context.setResolveBindings(true);
        context.scan(scanPaths, Collections.emptySet());

        HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
        Map<String, List<click.kamil.springstatemachineexporter.analysis.model.CallEdge>> callGraph =
                engine.buildCallGraph();

        EntryPoint entryPoint = EntryPoint.builder()
                .className("com.example.GenericCommandController")
                .methodName("execute")
                .name("POST /api/commands/{commandKey}")
                .parameters(List.of(EntryPoint.Parameter.builder()
                        .name("commandKey")
                        .type("String")
                        .annotations(List.of("PathVariable"))
                        .build()))
                .build();

        List<Map<String, String>> variants =
                EntryPointBindingExpander.expandPathVariableBindings(entryPoint, context, callGraph);

        assertThat(variants).extracting(map -> map.get("commandKey"))
                .containsExactlyInAnyOrder("order.pay", "order.ship");
    }

    @Test
    void shouldExpandCommandKeyFromCrossModuleStaticFactoryReturningMapOf(@TempDir Path tempDir) throws Exception {
        Path root = tempDir.resolve("order-system");
        Files.createDirectories(root);
        Files.writeString(root.resolve("settings.gradle"), "include 'api-module', 'core-module'");

        Path apiModule = root.resolve("api-module");
        Files.createDirectories(apiModule.resolve("src/main/java/com/example/routes"));
        Files.writeString(apiModule.resolve("build.gradle"), "");
        Files.writeString(apiModule.resolve("src/main/java/com/example/routes/CommandRoutes.java"), """
                package com.example.routes;
                import java.util.Map;
                public class CommandRoutes {
                    public static Map<String, String> seed() {
                        return Map.of(
                                "order.pay", "OrderEvent.PAY",
                                "order.ship", "OrderEvent.SHIP");
                    }
                }
                """);

        Path coreModule = root.resolve("core-module");
        Files.createDirectories(coreModule.resolve("src/main/java/com/example"));
        Files.writeString(coreModule.resolve("build.gradle"),
                "dependencies { implementation project(':api-module') }");
        Files.writeString(coreModule.resolve("src/main/java/com/example/GenericCommandController.java"), """
                package com.example;
                import com.example.routes.CommandRoutes;
                import java.util.Map;
                public class GenericCommandController {
                    OrderGateway gateway;
                    public void execute(String commandKey) {
                        gateway.trigger(commandKey);
                    }
                }
                class OrderGateway {
                    private static final Map<String, OrderEvent> ROUTES = CommandRoutes.seed();
                    void trigger(String commandKey) {
                        OrderEvent event = OrderEvent.valueOf(ROUTES.get(commandKey));
                        StateMachine sm = new StateMachine();
                        sm.sendEvent(event);
                    }
                }
                enum OrderEvent { PAY, SHIP }
                class StateMachine { void sendEvent(OrderEvent event) {} }
                """);

        SiblingDependencyResolver siblingResolver = new SiblingDependencyResolver();
        ProjectModuleGraph graph = siblingResolver.analyzeProject(coreModule);
        Set<Path> scanPaths = graph.resolveScanPaths(coreModule, false);

        CodebaseContext context = new CodebaseContext();
        context.setResolveBindings(true);
        context.scan(scanPaths, Collections.emptySet());

        HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
        Map<String, List<click.kamil.springstatemachineexporter.analysis.model.CallEdge>> callGraph =
                engine.buildCallGraph();

        EntryPoint entryPoint = EntryPoint.builder()
                .className("com.example.GenericCommandController")
                .methodName("execute")
                .name("POST /api/commands/{commandKey}")
                .parameters(List.of(EntryPoint.Parameter.builder()
                        .name("commandKey")
                        .type("String")
                        .annotations(List.of("PathVariable"))
                        .build()))
                .build();

        List<Map<String, String>> variants =
                EntryPointBindingExpander.expandPathVariableBindings(entryPoint, context, callGraph);

        assertThat(variants).extracting(map -> map.get("commandKey"))
                .containsExactlyInAnyOrder("order.pay", "order.ship");
    }

    @Test
    void shouldExpandCommandKeyFromInheritedStaticBlockMapInSiblingModule(@TempDir Path tempDir) throws Exception {
        Path root = tempDir.resolve("order-system");
        Files.createDirectories(root);
        Files.writeString(root.resolve("settings.gradle"), "include 'api-module', 'core-module'");

        Path apiModule = root.resolve("api-module");
        Files.createDirectories(apiModule.resolve("src/main/java/com/example/routes"));
        Files.writeString(apiModule.resolve("build.gradle"), "");
        Files.writeString(apiModule.resolve("src/main/java/com/example/routes/BaseRoutes.java"), """
                package com.example.routes;
                import java.util.HashMap;
                import java.util.Map;
                public abstract class BaseRoutes {
                    protected static final Map<String, String> ROUTES;
                    static {
                        ROUTES = new HashMap<>();
                        ROUTES.put("order.pay", "OrderEvent.PAY");
                        ROUTES.put("order.ship", "OrderEvent.SHIP");
                    }
                }
                """);

        Path coreModule = root.resolve("core-module");
        Files.createDirectories(coreModule.resolve("src/main/java/com/example"));
        Files.writeString(coreModule.resolve("build.gradle"),
                "dependencies { implementation project(':api-module') }");
        Files.writeString(coreModule.resolve("src/main/java/com/example/GenericCommandController.java"), """
                package com.example;
                import com.example.routes.BaseRoutes;
                public class GenericCommandController {
                    OrderGateway gateway;
                    public void execute(String commandKey) {
                        gateway.trigger(commandKey);
                    }
                }
                class OrderGateway extends BaseRoutes {
                    void trigger(String commandKey) {
                        OrderEvent event = OrderEvent.valueOf(ROUTES.get(commandKey));
                        StateMachine sm = new StateMachine();
                        sm.sendEvent(event);
                    }
                }
                enum OrderEvent { PAY, SHIP }
                class StateMachine { void sendEvent(OrderEvent event) {} }
                """);

        SiblingDependencyResolver siblingResolver = new SiblingDependencyResolver();
        ProjectModuleGraph graph = siblingResolver.analyzeProject(coreModule);
        Set<Path> scanPaths = graph.resolveScanPaths(coreModule, false);

        CodebaseContext context = new CodebaseContext();
        context.setResolveBindings(true);
        context.scan(scanPaths, Collections.emptySet());

        HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
        Map<String, List<click.kamil.springstatemachineexporter.analysis.model.CallEdge>> callGraph =
                engine.buildCallGraph();

        EntryPoint entryPoint = EntryPoint.builder()
                .className("com.example.GenericCommandController")
                .methodName("execute")
                .name("POST /api/commands/{commandKey}")
                .parameters(List.of(EntryPoint.Parameter.builder()
                        .name("commandKey")
                        .type("String")
                        .annotations(List.of("PathVariable"))
                        .build()))
                .build();

        List<Map<String, String>> variants =
                EntryPointBindingExpander.expandPathVariableBindings(entryPoint, context, callGraph);

        assertThat(variants).extracting(map -> map.get("commandKey"))
                .containsExactlyInAnyOrder("order.pay", "order.ship");
    }

    @Test
    void shouldExpandCommandKeyFromQualifiedInheritedStaticMapInSiblingModule(@TempDir Path tempDir) throws Exception {
        Path root = tempDir.resolve("order-system");
        Files.createDirectories(root);
        Files.writeString(root.resolve("settings.gradle"), "include 'api-module', 'core-module'");

        Path apiModule = root.resolve("api-module");
        Files.createDirectories(apiModule.resolve("src/main/java/com/example/routes"));
        Files.writeString(apiModule.resolve("build.gradle"), "");
        Files.writeString(apiModule.resolve("src/main/java/com/example/routes/BaseRoutes.java"), """
                package com.example.routes;
                import java.util.Map;
                public abstract class BaseRoutes {
                    protected static final Map<String, String> ROUTES = Map.of(
                            "order.pay", "OrderEvent.PAY",
                            "order.ship", "OrderEvent.SHIP");
                }
                """);

        Path coreModule = root.resolve("core-module");
        Files.createDirectories(coreModule.resolve("src/main/java/com/example"));
        Files.writeString(coreModule.resolve("build.gradle"),
                "dependencies { implementation project(':api-module') }");
        Files.writeString(coreModule.resolve("src/main/java/com/example/GenericCommandController.java"), """
                package com.example;
                import com.example.routes.BaseRoutes;
                public class GenericCommandController {
                    OrderGateway gateway;
                    public void execute(String commandKey) {
                        gateway.trigger(commandKey);
                    }
                }
                class OrderGateway extends BaseRoutes {
                    void trigger(String commandKey) {
                        OrderEvent event = OrderEvent.valueOf(BaseRoutes.ROUTES.get(commandKey));
                        StateMachine sm = new StateMachine();
                        sm.sendEvent(event);
                    }
                }
                enum OrderEvent { PAY, SHIP }
                class StateMachine { void sendEvent(OrderEvent event) {} }
                """);

        SiblingDependencyResolver siblingResolver = new SiblingDependencyResolver();
        ProjectModuleGraph graph = siblingResolver.analyzeProject(coreModule);
        Set<Path> scanPaths = graph.resolveScanPaths(coreModule, false);

        CodebaseContext context = new CodebaseContext();
        context.setResolveBindings(true);
        context.scan(scanPaths, Collections.emptySet());

        HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
        Map<String, List<click.kamil.springstatemachineexporter.analysis.model.CallEdge>> callGraph =
                engine.buildCallGraph();

        EntryPoint entryPoint = EntryPoint.builder()
                .className("com.example.GenericCommandController")
                .methodName("execute")
                .name("POST /api/commands/{commandKey}")
                .parameters(List.of(EntryPoint.Parameter.builder()
                        .name("commandKey")
                        .type("String")
                        .annotations(List.of("PathVariable"))
                        .build()))
                .build();

        List<Map<String, String>> variants =
                EntryPointBindingExpander.expandPathVariableBindings(entryPoint, context, callGraph);

        assertThat(variants).extracting(map -> map.get("commandKey"))
                .containsExactlyInAnyOrder("order.pay", "order.ship");
    }

    @Test
    void shouldExpandCommandKeyFromStaticMethodMapReceiverInSiblingModule(@TempDir Path tempDir) throws Exception {
        Path root = tempDir.resolve("order-system");
        Files.createDirectories(root);
        Files.writeString(root.resolve("settings.gradle"), "include 'api-module', 'core-module'");

        Path apiModule = root.resolve("api-module");
        Files.createDirectories(apiModule.resolve("src/main/java/com/example/routes"));
        Files.writeString(apiModule.resolve("build.gradle"), "");
        Files.writeString(apiModule.resolve("src/main/java/com/example/routes/CommandRoutes.java"), """
                package com.example.routes;
                import java.util.Map;
                public class CommandRoutes {
                    public static Map<String, String> routes() {
                        return Map.of(
                                "order.pay", "OrderEvent.PAY",
                                "order.ship", "OrderEvent.SHIP");
                    }
                }
                """);

        Path coreModule = root.resolve("core-module");
        Files.createDirectories(coreModule.resolve("src/main/java/com/example"));
        Files.writeString(coreModule.resolve("build.gradle"),
                "dependencies { implementation project(':api-module') }");
        Files.writeString(coreModule.resolve("src/main/java/com/example/GenericCommandController.java"), """
                package com.example;
                import com.example.routes.CommandRoutes;
                public class GenericCommandController {
                    OrderGateway gateway;
                    public void execute(String commandKey) {
                        gateway.trigger(commandKey);
                    }
                }
                class OrderGateway {
                    void trigger(String commandKey) {
                        OrderEvent event = OrderEvent.valueOf(CommandRoutes.routes().get(commandKey));
                        StateMachine sm = new StateMachine();
                        sm.sendEvent(event);
                    }
                }
                enum OrderEvent { PAY, SHIP }
                class StateMachine { void sendEvent(OrderEvent event) {} }
                """);

        SiblingDependencyResolver siblingResolver = new SiblingDependencyResolver();
        ProjectModuleGraph graph = siblingResolver.analyzeProject(coreModule);
        Set<Path> scanPaths = graph.resolveScanPaths(coreModule, false);

        CodebaseContext context = new CodebaseContext();
        context.setResolveBindings(true);
        context.scan(scanPaths, Collections.emptySet());

        HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
        Map<String, List<click.kamil.springstatemachineexporter.analysis.model.CallEdge>> callGraph =
                engine.buildCallGraph();

        EntryPoint entryPoint = EntryPoint.builder()
                .className("com.example.GenericCommandController")
                .methodName("execute")
                .name("POST /api/commands/{commandKey}")
                .parameters(List.of(EntryPoint.Parameter.builder()
                        .name("commandKey")
                        .type("String")
                        .annotations(List.of("PathVariable"))
                        .build()))
                .build();

        List<Map<String, String>> variants =
                EntryPointBindingExpander.expandPathVariableBindings(entryPoint, context, callGraph);

        assertThat(variants).extracting(map -> map.get("commandKey"))
                .containsExactlyInAnyOrder("order.pay", "order.ship");
    }

    @Test
    void shouldExpandRequestParamFromStaticMapLookupInSiblingModule(@TempDir Path tempDir) throws Exception {
        Path root = tempDir.resolve("order-system");
        Files.createDirectories(root);
        Files.writeString(root.resolve("settings.gradle"), "include 'api-module', 'core-module'");

        Path apiModule = root.resolve("api-module");
        Files.createDirectories(apiModule.resolve("src/main/java/com/example/routes"));
        Files.writeString(apiModule.resolve("build.gradle"), "");
        Files.writeString(apiModule.resolve("src/main/java/com/example/routes/CommandRoutes.java"), """
                package com.example.routes;
                import java.util.Map;
                public class CommandRoutes {
                    public static final Map<String, String> ROUTES = Map.of(
                            "order.pay", "OrderEvent.PAY",
                            "order.ship", "OrderEvent.SHIP");
                }
                """);

        Path coreModule = root.resolve("core-module");
        Files.createDirectories(coreModule.resolve("src/main/java/com/example"));
        Files.writeString(coreModule.resolve("build.gradle"),
                "dependencies { implementation project(':api-module') }");
        Files.writeString(coreModule.resolve("src/main/java/com/example/GenericCommandController.java"), """
                package com.example;
                import com.example.routes.CommandRoutes;
                public class GenericCommandController {
                    OrderGateway gateway;
                    public void execute(String commandKey) {
                        gateway.trigger(commandKey);
                    }
                }
                class OrderGateway {
                    void trigger(String commandKey) {
                        OrderEvent event = OrderEvent.valueOf(CommandRoutes.ROUTES.get(commandKey));
                        StateMachine sm = new StateMachine();
                        sm.sendEvent(event);
                    }
                }
                enum OrderEvent { PAY, SHIP }
                class StateMachine { void sendEvent(OrderEvent event) {} }
                """);

        SiblingDependencyResolver siblingResolver = new SiblingDependencyResolver();
        ProjectModuleGraph graph = siblingResolver.analyzeProject(coreModule);
        Set<Path> scanPaths = graph.resolveScanPaths(coreModule, false);

        CodebaseContext context = new CodebaseContext();
        context.setResolveBindings(true);
        context.scan(scanPaths, Collections.emptySet());

        HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
        Map<String, List<click.kamil.springstatemachineexporter.analysis.model.CallEdge>> callGraph =
                engine.buildCallGraph();

        EntryPoint entryPoint = EntryPoint.builder()
                .className("com.example.GenericCommandController")
                .methodName("execute")
                .name("POST /api/commands")
                .parameters(List.of(EntryPoint.Parameter.builder()
                        .name("commandKey")
                        .type("String")
                        .annotations(List.of("RequestParam"))
                        .build()))
                .build();

        List<Map<String, String>> variants =
                EntryPointBindingExpander.expandPathVariableBindings(entryPoint, context, callGraph);

        assertThat(variants).extracting(map -> map.get("commandKey"))
                .containsExactlyInAnyOrder("order.pay", "order.ship");
        assertThat(EntryPointBindingExpander.withResolvedPath(entryPoint, variants.get(0)).getName())
                .isEqualTo("POST /api/commands?commandKey=order.pay");
    }
}
