package click.kamil.springstatemachineexporter.analysis.service;

import click.kamil.springstatemachineexporter.analysis.model.CallChain;
import click.kamil.springstatemachineexporter.analysis.model.CallEdge;
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import click.kamil.springstatemachineexporter.ast.common.DataFlowModel;
import click.kamil.springstatemachineexporter.ast.common.JdtDataFlowModel;
import org.eclipse.jdt.core.dom.Expression;
import org.eclipse.jdt.core.dom.ExpressionStatement;
import org.eclipse.jdt.core.dom.LambdaExpression;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.MethodInvocation;
import org.eclipse.jdt.core.dom.TypeDeclaration;
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
import org.eclipse.jdt.core.dom.VariableDeclarationStatement;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

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

/**
 * Regression suite for functional-interface event resolution ({@code Supplier.get()},
 * {@code Runnable.run()}, {@code Consumer.accept()}, {@code Callable.call()}, side-effect lambdas).
 * Must stay on the dataflow pipeline:
 * {@link JdtDataFlowModel} → {@link VariableTracer} → {@link PathBindingEvaluator}
 * → {@link PathBindingExpressionResolver} → call graph.
 * Do not reintroduce a parallel {@code CallSiteArgumentResolver} bridge.
 */
class FunctionalInterfaceDataFlowRegressionTest {

    @Nested
    class JdtDataFlowModelLayer {

        @Test
        void shouldResolveEnumFromSupplierGetOnLocalLambda(@TempDir Path tempDir) throws IOException {
            CodebaseContext context = scan(tempDir, """
                    package com.example;
                    import java.util.function.Supplier;
                    public enum OrderCommand { PAY, SHIP }
                    public class Hooks {
                        OrderCommand read() {
                            Supplier<OrderCommand> supplier = () -> OrderCommand.PAY;
                            return supplier.get();
                        }
                    }
                    """);

            MethodInvocation getCall = findReturnMethodInvocation(context, "com.example.Hooks", "read");
            assertThat(getCall.getName().getIdentifier()).isEqualTo("get");

            DataFlowModel model = new JdtDataFlowModel(context);
            List<String> constants = new ArrayList<>();
            for (Expression def : model.getReachingDefinitions(getCall)) {
                new ConstantExtractor(context, context.getConstantResolver(), null)
                        .extractConstantsFromExpression(def, constants);
            }
            assertThat(constants).anyMatch(c -> c.contains("PAY"));
        }

        @Test
        void shouldResolveEnumLiteralThroughDataflow(@TempDir Path tempDir) throws IOException {
            CodebaseContext context = scan(tempDir, """
                    package com.example;
                    public enum OrderCommand { PAY, SHIP }
                    public class Service {
                        public void fireEvent() {
                            sendEvent(OrderCommand.PAY);
                        }
                        protected void sendEvent(OrderCommand event) {}
                    }
                    """);

            MethodInvocation sendEventCall = findSideEffectCall(context, "com.example.Service", "fireEvent");
            Expression eventArg = (Expression) sendEventCall.arguments().get(0);

            DataFlowModel model = new JdtDataFlowModel(context);
            List<String> fromDataflow = new ArrayList<>();
            for (Expression def : model.getReachingDefinitions(eventArg)) {
                new ConstantExtractor(context, context.getConstantResolver(), null)
                        .extractConstantsFromExpression(def, fromDataflow);
            }
            assertThat(fromDataflow).anyMatch(c -> c.contains("PAY"));
        }

        @Test
        void shouldNotTreatPlainObjectGetAsFunctionalSupplier(@TempDir Path tempDir) throws IOException {
            CodebaseContext context = scan(tempDir, """
                    package com.example;
                    public class Runner {
                        static class Box {
                            public String get() { return "X"; }
                        }
                        public void run() {
                            Box box = new Box();
                            String value = box.get();
                        }
                    }
                    """);

            DataFlowModel model = new JdtDataFlowModel(context);
            Expression initializer = findInitializer(context, "com.example.Runner", "run", "value");
            assertThat(model.resolveValue(initializer, context)).isEqualTo("X");
        }

        @Test
        void shouldResolveEnumFromCallableCallOnLocalLambda(@TempDir Path tempDir) throws IOException {
            CodebaseContext context = scan(tempDir, """
                    package com.example;
                    import java.util.concurrent.Callable;
                    public enum OrderCommand { PAY, SHIP }
                    public class Hooks {
                        OrderCommand read() throws Exception {
                            Callable<OrderCommand> callable = () -> OrderCommand.PAY;
                            return callable.call();
                        }
                    }
                    """);

            MethodInvocation call = findReturnMethodInvocation(context, "com.example.Hooks", "read");
            assertThat(call.getName().getIdentifier()).isEqualTo("call");

            DataFlowModel model = new JdtDataFlowModel(context);
            List<String> constants = new ArrayList<>();
            for (Expression def : model.getReachingDefinitions(call)) {
                new ConstantExtractor(context, context.getConstantResolver(), null)
                        .extractConstantsFromExpression(def, constants);
            }
            assertThat(constants).anyMatch(c -> c.contains("PAY"));
        }

        @Test
        void shouldResolveEnumFromConsumerAcceptSideEffectLambda(@TempDir Path tempDir) throws IOException {
            CodebaseContext context = scan(tempDir, """
                    package com.example;
                    import java.util.function.Consumer;
                    public enum OrderCommand { PAY, SHIP }
                    public class Hooks {
                        void run() {
                            Consumer<String> handler = ignored -> sendEvent(OrderCommand.PAY);
                            handler.accept("trigger");
                        }
                        void sendEvent(OrderCommand event) {}
                    }
                    """);

            MethodInvocation acceptCall = findSideEffectCall(context, "com.example.Hooks", "run");
            assertThat(acceptCall.getName().getIdentifier()).isEqualTo("accept");

            DataFlowModel model = new JdtDataFlowModel(context);
            List<String> constants = new ArrayList<>();
            for (Expression def : model.getReachingDefinitions(acceptCall)) {
                new ConstantExtractor(context, context.getConstantResolver(), null)
                        .extractConstantsFromExpression(def, constants);
            }
            assertThat(constants).anyMatch(c -> c.contains("PAY"));
        }

        @Test
        void shouldResolveEnumThroughReactiveTransformLambdaBinding(@TempDir Path tempDir) throws IOException {
            CodebaseContext context = scan(tempDir, """
                    package com.example;
                    public enum OrderCommand { PAY, SHIP }
                    public class Hooks {
                        OrderCommand read() {
                            return Mono.just(OrderCommand.PAY).map(x -> x);
                        }
                    }
                    class Mono {
                        static Mono just(Object o) { return new Mono(); }
                        OrderCommand map(java.util.function.Function fn) { return OrderCommand.PAY; }
                    }
                    """);

            MethodInvocation mapCall = findReturnMethodInvocation(context, "com.example.Hooks", "read");
            assertThat(mapCall.getName().getIdentifier()).isEqualTo("map");

            DataFlowModel model = new JdtDataFlowModel(context);
            List<String> constants = new ArrayList<>();
            for (Expression def : model.getReachingDefinitions(mapCall)) {
                new ConstantExtractor(context, context.getConstantResolver(), null)
                        .extractConstantsFromExpression(def, constants);
            }
            assertThat(constants).anyMatch(c -> c.contains("PAY"));
        }
    }

    @Nested
    class VariableTracerLayer {

        @Test
        void shouldResolveSupplierLambdaAtCallSite(@TempDir Path tempDir) throws IOException {
            RunnableProject project = RunnableProject.supplierOnly(tempDir);
            VariableTracer tracer = tracer(project.context());

            List<String> constants = tracer.resolveConstantsFromCallSiteArgument(
                    "com.example.web.OrderController.process",
                    "com.example.service.OrderService.fireEvent",
                    0);
            assertThat(constants).containsExactly("OrderCommand.PAY");
        }

        @Test
        void shouldResolveRunnableSideEffectLambdaAtCallSite(@TempDir Path tempDir) throws IOException {
            RunnableProject project = RunnableProject.withExecutor(tempDir);
            VariableTracer tracer = tracer(project.context());

            assertThat(tracer.findFunctionalParameterIndex("com.example.executor.TaskExecutor.execute"))
                    .isZero();
            assertThat(tracer.findCallSiteArgumentExpression(
                    "com.example.web.OrderController.process",
                    "com.example.executor.TaskExecutor.execute",
                    0)).isInstanceOf(LambdaExpression.class);

            List<String> constants = tracer.resolveConstantsFromCallSiteArgument(
                    "com.example.service.AbstractOrderService.fireEvent",
                    "com.example.service.AbstractOrderService.sendEvent",
                    0);
            assertThat(constants).containsExactly("OrderCommand.PAY");
        }
    }

    @Nested
    class PathBindingEvaluatorLayer {

        @Test
        void shouldPropagateRunnableLambdaEnumThroughPathBindings(@TempDir Path tempDir) throws IOException {
            RunnableProject project = RunnableProject.withExecutor(tempDir);
            HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(project.context());
            Map<String, List<CallEdge>> graph = engine.buildCallGraph();
            CallGraphPathFinder pathFinder = new CallGraphPathFinder(project.context());
            PathBindingEvaluator evaluator = new PathBindingEvaluator(
                    project.context(), engine.variableTracer, project.context().getConstantResolver(), engine.typeResolver);

            List<String> path = List.of(
                    "com.example.web.OrderController.process",
                    "com.example.executor.TaskExecutor.execute",
                    runnableRunHop(graph),
                    "com.example.service.AbstractOrderService.fireEvent",
                    "com.example.service.AbstractOrderService.sendEvent");

            Map<String, String> bindings = evaluator.traceBindings(path, graph, pathFinder);
            assertThat(bindings.values()).anyMatch(v -> v.contains("PAY"));
        }

        @Test
        void shouldPropagateSupplierLambdaThroughParameterMap(@TempDir Path tempDir) throws IOException {
            RunnableProject project = RunnableProject.supplierOnly(tempDir);
            HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(project.context());
            Map<String, List<CallEdge>> graph = engine.buildCallGraph();

            Map<String, String> paramValues = engine.variableTracer.buildParameterValuesMap(
                    "com.example.web.OrderController.process",
                    "com.example.service.OrderService.fireEvent",
                    graph,
                    List.of(
                            "com.example.web.OrderController.process",
                            "com.example.service.OrderService.fireEvent"),
                    1);

            assertThat(paramValues.get("provider")).contains("PAY");
        }
    }

    @Nested
    class CallGraphEndToEndLayer {

        @Test
        void shouldLinkSupplierLambdaToSingleEnum(@TempDir Path tempDir) throws IOException {
            CodebaseContext context = scan(tempDir, """
                    package com.example;
                    import java.util.function.Supplier;
                    public class MyController {
                        private StateMachine machine;
                        public void process() {
                            fireEvent(() -> TransitionEnum.STATE_L);
                        }
                        private void fireEvent(Supplier<TransitionEnum> eventSupplier) {
                            machine.fire(eventSupplier.get());
                        }
                    }
                    class StateMachine {
                        public void fire(TransitionEnum event) {}
                    }
                    enum TransitionEnum { STATE_L, STATE_M }
                    """);

            HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
            List<CallChain> chains = engine.findChains(
                    List.of(EntryPoint.builder()
                            .className("com.example.MyController")
                            .methodName("process")
                            .build()),
                    List.of(TriggerPoint.builder()
                            .className("com.example.StateMachine")
                            .methodName("fire")
                            .event("event")
                            .build()));
            assertThat(chains).hasSize(1);
            assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents())
                    .containsExactly("TransitionEnum.STATE_L");
        }

        @Test
        void shouldLinkCrossClassSupplierLambda(@TempDir Path tempDir) throws IOException {
            Path root = tempDir.resolve("cross");
            Files.createDirectories(root);
            Files.writeString(root.resolve("JmsListener.java"), """
                    package com.example.web;
                    import com.example.service.StateMachineService;
                    import com.example.domain.OrderEvent;
                    public class JmsOrderListener {
                        private final StateMachineService service;
                        public JmsOrderListener(StateMachineService service) { this.service = service; }
                        public void receive() { service.sendWithProvider(() -> OrderEvent.CANCEL); }
                    }
                    """);
            Files.writeString(root.resolve("Service.java"), """
                    package com.example.service;
                    import com.example.domain.OrderEvent;
                    import java.util.function.Supplier;
                    public class StateMachineService {
                        public <T extends OrderEvent> void sendWithProvider(Supplier<T> provider) {
                            OrderEvent event = provider.get();
                            fire(event);
                        }
                        private void fire(OrderEvent event) {}
                    }
                    """);
            Files.writeString(root.resolve("OrderEvent.java"), """
                    package com.example.domain;
                    public enum OrderEvent { CANCEL, PROCESS }
                    """);

            CodebaseContext context = new CodebaseContext();
            context.scan(root);
            HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
            List<CallChain> chains = engine.findChains(
                    List.of(EntryPoint.builder()
                            .className("com.example.web.JmsOrderListener")
                            .methodName("receive")
                            .build()),
                    List.of(TriggerPoint.builder()
                            .className("com.example.service.StateMachineService")
                            .methodName("fire")
                            .event("event")
                            .build()));
            assertThat(chains).hasSize(1);
            assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactly("OrderEvent.CANCEL");
        }

        @Test
        void shouldLinkRunnableExecuteSideEffectLambda(@TempDir Path tempDir) throws IOException {
            RunnableProject project = RunnableProject.withExecutor(tempDir);
            assertSinglePayEvent(chains(project, "com.example.service.AbstractOrderService", "sendEvent"));
        }

        @Test
        void shouldLinkConsumerAcceptSideEffectLambda(@TempDir Path tempDir) throws IOException {
            CodebaseContext context = scan(tempDir, """
                    package com.example;
                    import java.util.function.Consumer;
                    public enum OrderCommand { PAY, SHIP }
                    public class OrderService {
                        private final EventDispatcher dispatcher = new EventDispatcher();
                        public void processOrder() {
                            dispatcher.dispatch("order", ignored -> fireEvent());
                        }
                        public void fireEvent() { sendEvent(OrderCommand.PAY); }
                        void sendEvent(OrderCommand event) {}
                    }
                    class EventDispatcher {
                        public void dispatch(String order, Consumer<String> callback) {
                            callback.accept("trigger");
                        }
                    }
                    """);

            HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
            List<CallChain> chains = engine.findChains(
                    List.of(EntryPoint.builder()
                            .className("com.example.OrderService")
                            .methodName("processOrder")
                            .build()),
                    List.of(TriggerPoint.builder()
                            .className("com.example.OrderService")
                            .methodName("sendEvent")
                            .event("event")
                            .build()));
            assertThat(chains).hasSize(1);
            assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents())
                    .containsExactly("OrderCommand.PAY");
        }

        @Test
        void shouldReachFireSiteThroughRunnableRunHop(@TempDir Path tempDir) throws IOException {
            RunnableProject project = RunnableProject.withExecutor(tempDir);
            HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(project.context());
            Map<String, List<CallEdge>> graph = engine.buildCallGraph();

            assertThat(graph.get("com.example.executor.TaskExecutor.execute"))
                    .extracting(CallEdge::getTargetMethod)
                    .anyMatch(target -> target.contains("Runnable.run"));

            List<CallChain> chains = engine.findChains(
                    List.of(EntryPoint.builder()
                            .className("com.example.web.OrderController")
                            .methodName("process")
                            .build()),
                    List.of(TriggerPoint.builder()
                            .className("com.example.service.AbstractOrderService")
                            .methodName("sendEvent")
                            .event("event")
                            .build()));
            assertThat(chains).hasSize(1);
            assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents())
                    .containsExactly("OrderCommand.PAY");
        }
    }

    private static void assertSinglePayEvent(List<CallChain> chains) {
        assertThat(chains).hasSize(1);
        assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactly("OrderCommand.PAY");
    }

    private static List<CallChain> chains(RunnableProject project, String triggerClass, String triggerMethod) {
        HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(project.context());
        return engine.findChains(
                List.of(EntryPoint.builder()
                        .className("com.example.web.OrderController")
                        .methodName("process")
                        .build()),
                List.of(TriggerPoint.builder()
                        .className(triggerClass)
                        .methodName(triggerMethod)
                        .event("event")
                        .build()));
    }

    private static String runnableRunHop(Map<String, List<CallEdge>> graph) {
        List<CallEdge> edges = graph.get("com.example.executor.TaskExecutor.execute");
        assertThat(edges).isNotNull();
        return edges.stream()
                .map(CallEdge::getTargetMethod)
                .filter(hop -> hop.endsWith("Runnable.run"))
                .findFirst()
                .orElseThrow();
    }

    private static VariableTracer tracer(CodebaseContext context) {
        VariableTracer tracer = new VariableTracer(context, context.getConstantResolver());
        TypeResolver typeResolver = new TypeResolver(context);
        ConstantExtractor constantExtractor = new ConstantExtractor(context, context.getConstantResolver(), null);
        tracer.setTypeResolver(typeResolver);
        tracer.setConstantExtractor(constantExtractor);
        return tracer;
    }

    private static CodebaseContext scan(Path tempDir, String source) throws IOException {
        Files.writeString(tempDir.resolve("App.java"), source);
        CodebaseContext context = new CodebaseContext();
        context.setResolveBindings(true);
        context.scan(tempDir);
        return context;
    }

    private static Expression findInitializer(
            CodebaseContext context, String typeFqn, String methodName, String variableName) {
        TypeDeclaration type = context.getTypeDeclaration(typeFqn);
        MethodDeclaration method = context.findMethodDeclaration(type, methodName, true);
        for (Object statementObj : method.getBody().statements()) {
            if (statementObj instanceof VariableDeclarationStatement statement) {
                for (Object fragmentObj : statement.fragments()) {
                    if (fragmentObj instanceof VariableDeclarationFragment fragment
                            && variableName.equals(fragment.getName().getIdentifier())) {
                        return fragment.getInitializer();
                    }
                }
            }
        }
        throw new AssertionError("Initializer not found: " + variableName);
    }

    private static MethodInvocation findMethodInvocationInitializer(
            CodebaseContext context, String typeFqn, String methodName, String variableName) {
        Expression initializer = findInitializer(context, typeFqn, methodName, variableName);
        assertThat(initializer).isInstanceOf(MethodInvocation.class);
        return (MethodInvocation) initializer;
    }

    private static MethodInvocation findSideEffectCall(CodebaseContext context, String typeFqn, String methodName) {
        TypeDeclaration type = context.getTypeDeclaration(typeFqn);
        MethodDeclaration method = context.findMethodDeclaration(type, methodName, true);
        for (Object statementObj : method.getBody().statements()) {
            if (statementObj instanceof ExpressionStatement statement
                    && statement.getExpression() instanceof MethodInvocation invocation) {
                return invocation;
            }
        }
        throw new AssertionError("Side-effect call not found in " + methodName);
    }

    private static MethodInvocation findReturnMethodInvocation(CodebaseContext context, String typeFqn, String methodName) {
        TypeDeclaration type = context.getTypeDeclaration(typeFqn);
        MethodDeclaration method = context.findMethodDeclaration(type, methodName, true);
        for (Object statementObj : method.getBody().statements()) {
            if (statementObj instanceof org.eclipse.jdt.core.dom.ReturnStatement returnStatement
                    && returnStatement.getExpression() instanceof MethodInvocation invocation) {
                return invocation;
            }
        }
        throw new AssertionError("Return MethodInvocation not found in " + methodName);
    }

    private record RunnableProject(CodebaseContext context) {

        static RunnableProject supplierOnly(Path tempDir) throws IOException {
            Path root = tempDir.resolve("supplier");
            Files.createDirectories(root);
            writeSharedSources(root, false);
            return new RunnableProject(scanRoot(root));
        }

        static RunnableProject withExecutor(Path tempDir) throws IOException {
            Path root = tempDir.resolve("runnable");
            Files.createDirectories(root);
            writeSharedSources(root, true);
            return new RunnableProject(scanRoot(root));
        }

        private static CodebaseContext scanRoot(Path root) throws IOException {
            CodebaseContext context = new CodebaseContext();
            context.setResolveBindings(true);
            context.scan(root);
            return context;
        }

        private static void writeSharedSources(Path root, boolean withExecutor) throws IOException {
            Files.writeString(root.resolve("OrderCommand.java"), """
                    package com.example.domain;
                    public enum OrderCommand { PAY, SHIP, META }
                    """);
            if (withExecutor) {
                Files.writeString(root.resolve("TaskExecutor.java"), """
                        package com.example.executor;
                        public class TaskExecutor {
                            public void execute(Runnable task) { task.run(); }
                        }
                        """);
                Files.writeString(root.resolve("OrderController.java"), """
                        package com.example.web;
                        import com.example.executor.TaskExecutor;
                        import com.example.service.AbstractOrderService;
                        public class OrderController {
                            private final TaskExecutor executor;
                            private final AbstractOrderService service;
                            public OrderController(TaskExecutor executor, AbstractOrderService service) {
                                this.executor = executor;
                                this.service = service;
                            }
                            public void process() {
                                executor.execute(() -> service.fireEvent());
                            }
                        }
                        """);
                Files.writeString(root.resolve("AbstractOrderService.java"), """
                        package com.example.service;
                        import com.example.domain.OrderCommand;
                        public abstract class AbstractOrderService {
                            protected com.example.machine.StateMachine machine;
                            public void fireEvent() { sendEvent(OrderCommand.PAY); }
                            protected void sendEvent(OrderCommand event) { machine.fire(event); }
                        }
                        """);
                Files.writeString(root.resolve("StateMachine.java"), """
                        package com.example.machine;
                        import com.example.domain.OrderCommand;
                        public class StateMachine {
                            public void fire(OrderCommand event) {}
                        }
                        """);
            } else {
                Files.writeString(root.resolve("OrderController.java"), """
                        package com.example.web;
                        import com.example.service.OrderService;
                        import com.example.domain.OrderCommand;
                        public class OrderController {
                            private final OrderService service;
                            public OrderController(OrderService service) { this.service = service; }
                            public void process() {
                                service.fireEvent(() -> OrderCommand.PAY);
                            }
                        }
                        """);
                Files.writeString(root.resolve("OrderService.java"), """
                        package com.example.service;
                        import com.example.domain.OrderCommand;
                        import com.example.machine.StateMachine;
                        import java.util.function.Supplier;
                        public class OrderService {
                            private StateMachine machine;
                            public void fireEvent(Supplier<OrderCommand> provider) {
                                machine.fire(provider.get());
                            }
                        }
                        """);
                Files.writeString(root.resolve("StateMachine.java"), """
                        package com.example.machine;
                        import com.example.domain.OrderCommand;
                        public class StateMachine {
                            public void fire(OrderCommand event) {}
                        }
                        """);
            }
        }
    }

    @Nested
    class GetterChainDataFlowLayer {

        @Test
        void shouldResolveChainedGetterThroughDataflowBeforeAccessorFallback(@TempDir Path tempDir) throws IOException {
            CodebaseContext context = scan(tempDir, """
                    package com.example;
                    public class OrderController {
                        private OrderService service;
                        public void processOrderEvent(EventWrapper wrapper) {
                            service.updateOrderState(wrapper.getEvent().getType());
                        }
                    }
                    class OrderService {
                        public void updateOrderState(OrderEvents event) {}
                    }
                    class EventWrapper {
                        public RichEvent getEvent() { return new RichEvent(); }
                    }
                    class RichEvent {
                        public OrderEvents getType() { return OrderEvents.PAY; }
                    }
                    enum OrderEvents { PAY }
                    """);

            HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
            List<CallChain> chains = engine.findChains(
                    List.of(EntryPoint.builder()
                            .className("com.example.OrderController")
                            .methodName("processOrderEvent")
                            .build()),
                    List.of(TriggerPoint.builder()
                            .className("com.example.OrderService")
                            .methodName("updateOrderState")
                            .event("event")
                            .build()));

            assertThat(chains).hasSize(1);
            assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents())
                    .containsExactlyInAnyOrder("OrderEvents.PAY");
        }
    }
}
