package click.kamil.springstatemachineexporter.analysis.service;

import click.kamil.springstatemachineexporter.analysis.model.CallChain;
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
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.List;

import org.junit.jupiter.api.Tag;
import static org.assertj.core.api.Assertions.assertThat;

@Tag("heuristic_guess_paths_and_scrape_regex_replace_with_data_flow_analysis")
class HeuristicCallGraphEngineGetterTest {

    @Test
    void shouldResolveConstantsFromAlternativeGetterName() throws IOException {
        String source = """
                package com.example;
                public class OrderController {
                    private OrderService service;
                    public void handleStatus(DomainEvent domainEvent) {
                        service.process(domainEvent.get2Type());
                    }
                }
                
                class DomainEvent {
                    public String get2Type() {
                        return "ALTERNATIVE_EVENT";
                    }
                }
                
                class OrderService {
                    public void process(String event) {}
                }
                """;
        Path tempDir = Files.createTempDirectory("callgraph_alt_getter");
        Files.writeString(tempDir.resolve("OrderConfig.java"), source);
        CodebaseContext context = new CodebaseContext();
        context.scan(tempDir);
        HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);

        EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build();
        TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
        List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
        
        org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
        org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("ALTERNATIVE_EVENT");
    }

    @Test
    void shouldResolveConstantsFromInlineInstantiation() throws IOException {
        String source = """
                package com.example;
                public class OrderController {
                    private OrderService service;
                    public void handleStatus() {
                        service.process(new DomainEvent().getEvent());
                    }
                }
                
                class DomainEvent {
                    public String getEvent() { return "INLINE_EVENT"; }
                }
                
                class OrderService {
                    public void process(String event) {}
                }
                """;
        Path tempDir = Files.createTempDirectory("callgraph_inline_inst");
        Files.writeString(tempDir.resolve("OrderConfig.java"), source);
        CodebaseContext context = new CodebaseContext();
        context.scan(tempDir);
        HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);

        EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build();
        TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
        List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
        
        org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
        org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("INLINE_EVENT");
    }

    @Test
    void shouldResolveConstantsFromInlineInstantiationWithArguments() throws IOException {
        String source = """
                package com.example;
                public class OrderController {
                    private OrderService service;
                    public void handleStatus() {
                        service.process(new DomainEvent("PAYLOAD_ID").getEvent());
                    }
                }
                
                class DomainEvent {
                    private String id;
                    public DomainEvent(String id) { this.id = id; }
                    public String getEvent() { return "INLINE_EVENT_WITH_ARGS"; }
                }
                
                class OrderService {
                    public void process(String event) {}
                }
                """;
        Path tempDir = Files.createTempDirectory("callgraph_inline_inst_args");
        Files.writeString(tempDir.resolve("OrderConfig.java"), source);
        CodebaseContext context = new CodebaseContext();
        context.scan(tempDir);
        HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);

        EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build();
        TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
        List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
        
        org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
        org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("INLINE_EVENT_WITH_ARGS");
    }

    @Test
    void shouldResolveConstantsFromInlineInstantiationWithGenerics() throws IOException {
        String source = """
                package com.example;
                public class OrderController {
                    private OrderService service;
                    public void handleStatus() {
                        service.process(new DomainEvent<String>().getEvent());
                    }
                }
                
                class DomainEvent<T> {
                    public String getEvent() { return "INLINE_EVENT_WITH_GENERICS"; }
                }
                
                class OrderService {
                    public void process(String event) {}
                }
                """;
        Path tempDir = Files.createTempDirectory("callgraph_inline_inst_gen");
        Files.writeString(tempDir.resolve("OrderConfig.java"), source);
        CodebaseContext context = new CodebaseContext();
        context.scan(tempDir);
        HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);

        EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build();
        TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
        List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
        
        org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
        org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("INLINE_EVENT_WITH_GENERICS");
    }

    @Test
    void shouldResolveConstantsFromInlineInstantiationInheritedMethod() throws IOException {
        String source = """
                package com.example;
                public class OrderController {
                    private OrderService service;
                    public void handleStatus() {
                        service.process(new SpecificDomainEvent().getEvent());
                    }
                }
                
                abstract class BaseDomainEvent {
                    public String getEvent() { return "BASE_EVENT_RETURN"; }
                }
                
                class SpecificDomainEvent extends BaseDomainEvent {
                    // no getEvent here, it inherits it!
                }
                
                class OrderService {
                    public void process(String event) {}
                }
                """;
        Path tempDir = Files.createTempDirectory("callgraph_inline_inst_hier");
        Files.writeString(tempDir.resolve("OrderConfig.java"), source);
        CodebaseContext context = new CodebaseContext();
        context.scan(tempDir);
        HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);

        EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build();
        TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
        List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
        
        org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
        org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("BASE_EVENT_RETURN");
    }

    @Test
    void shouldResolveConstantsFromInlineInstantiationWithComplexArguments() throws IOException {
        String source = """
                package com.example;
                public class OrderController {
                    private OrderService service;
                    public void handleStatus(Order o, String abc) {
                        service.process(new DomainEvent(o.getId(), abc).getEvent());
                    }
                }
                
                class Order {
                    public String getId() { return "123"; }
                }
                
                class DomainEvent {
                    private String id;
                    private String extra;
                    public DomainEvent(String id, String extra) { this.id = id; this.extra = extra; }
                    public String getEvent() { return "INLINE_EVENT_COMPLEX_ARGS"; }
                }
                
                class OrderService {
                    public void process(String event) {}
                }
                """;
        Path tempDir = Files.createTempDirectory("callgraph_inline_inst_complex_args");
        Files.writeString(tempDir.resolve("OrderConfig.java"), source);
        CodebaseContext context = new CodebaseContext();
        context.scan(tempDir);
        HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);

        EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build();
        TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
        List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
        
        org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
        org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("INLINE_EVENT_COMPLEX_ARGS");
    }

    @Test
    void shouldNotIncludeSubclassesForInlineInstantiation() throws IOException {
        String source = """
                package com.example;
                public class OrderController {
                    private OrderService service;
                    public void handleStatus() {
                        service.process(new BaseEvent().getEvent());
                    }
                }
                
                class BaseEvent {
                    public String getEvent() { return "BASE_EVENT_RETURN"; }
                }
                
                class SubEvent extends BaseEvent {
                    public String getEvent() { return "SUB_EVENT_RETURN"; }
                }
                
                class OrderService {
                    public void process(String event) {}
                }
                """;
        Path tempDir = Files.createTempDirectory("callgraph_inline_inst_subclass");
        Files.writeString(tempDir.resolve("OrderConfig.java"), source);
        CodebaseContext context = new CodebaseContext();
        context.scan(tempDir);
        HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);

        EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build();
        TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
        List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
        
        org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
        // Only BASE_EVENT_RETURN should be found. SUB_EVENT_RETURN is a subclass and should not be included for direct instantiations
        org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("BASE_EVENT_RETURN");
    }

    @Test
    void shouldResolveConstantsFromFieldAssignedInConstructor() throws IOException {
        String source = """
                package com.example;
                public class OrderController {
                    private OrderService service;
                    public void handleStatus() {
                        service.process(new OrderPickedUpEvent().getType());
                    }
                }
                
                class OrderPickedUpEvent {
                    private String type;
                    public OrderPickedUpEvent() {
                        this.type = "PICKED_UP_IN_CONSTRUCTOR";
                    }
                    public String getType() { return type; }
                }
                
                class OrderService {
                    public void process(String event) {}
                }
                """;
        Path tempDir = Files.createTempDirectory("callgraph_field_const");
        Files.writeString(tempDir.resolve("OrderConfig.java"), source);
        CodebaseContext context = new CodebaseContext();
        context.scan(tempDir);
        HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);

        EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build();
        TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
        List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
        
        org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
        org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("PICKED_UP_IN_CONSTRUCTOR");
    }

    @Test
    void shouldResolveConstantsFromFieldAssignedInSuperConstructor() throws IOException {
        String source = """
                package com.example;
                public class OrderController {
                    private OrderService service;
                    public void handleStatus() {
                        service.process(new OrderCancelledEvent().getType());
                    }
                }
                
                abstract class BaseEvent {
                    private String type;
                    public BaseEvent(String type, String otherVar) {
                        this.type = type;
                    }
                    public String getType() { return type; }
                }
                
                class OrderCancelledEvent extends BaseEvent {
                    public OrderCancelledEvent() {
                        super("CANCELLED_IN_SUPER", "someVar");
                    }
                }
                
                class OrderService {
                    public void process(String event) {}
                }
                """;
        Path tempDir = Files.createTempDirectory("callgraph_super_const");
        Files.writeString(tempDir.resolve("OrderConfig.java"), source);
        CodebaseContext context = new CodebaseContext();
        context.scan(tempDir);
        HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);

        EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build();
        TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
        List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
        
        org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
        org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).contains("CANCELLED_IN_SUPER");
    }

    @Test
    void shouldResolveConstantsFromFieldAssignedToAnotherConstantInConstructor() throws IOException {
        String source = """
                package com.example;
                public class OrderController {
                    private OrderService service;
                    public void handleStatus() {
                        service.process(new EventWithIndirectConstant().getType());
                    }
                }
                
                class EventConstants {
                    public static final String INDIRECT_EVENT = "INDIRECT_EVENT_VALUE";
                }
                
                class EventWithIndirectConstant {
                    private String type;
                    public EventWithIndirectConstant() {
                        this.type = EventConstants.INDIRECT_EVENT;
                    }
                    public String getType() { return type; }
                }
                
                class OrderService {
                    public void process(String event) {}
                }
                """;
        Path tempDir = Files.createTempDirectory("callgraph_indirect_const");
        Files.writeString(tempDir.resolve("OrderConfig.java"), source);
        CodebaseContext context = new CodebaseContext();
        context.scan(tempDir);
        HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);

        EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build();
        TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
        List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
        
        org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
        org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).contains("INDIRECT_EVENT_VALUE");
    }

    @Test
    void shouldResolveConstantsFromConstructorInvocation() throws IOException {
        String source = """
                package com.example;
                public class OrderController {
                    private OrderService service;
                    public void handleStatus() {
                        service.process(new EventWithThisCall().getType());
                    }
                }
                
                class EventWithThisCall {
                    private String type;
                    public EventWithThisCall() {
                        this("THIS_CALL_CONSTANT");
                    }
                    public EventWithThisCall(String type) {
                        this.type = type;
                    }
                    public String getType() { return type; }
                }
                
                class OrderService {
                    public void process(String event) {}
                }
                """;
        Path tempDir = Files.createTempDirectory("callgraph_this_const");
        Files.writeString(tempDir.resolve("OrderConfig.java"), source);
        CodebaseContext context = new CodebaseContext();
        context.scan(tempDir);
        HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);

        EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build();
        TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
        List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
        
        org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
        org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).contains("THIS_CALL_CONSTANT");
    }

    @Test
    void shouldResolveConstantsFromDynamicMethodAssignmentInConstructor() throws IOException {
        String source = """
                package com.example;
                public class OrderController {
                    private OrderService service;
                    public void handleStatus() {
                        service.process(new OrderCancelledEvent().getType());
                    }
                }
                
                class OrderCancelledEvent {
                    private String cancelEventType;
                    public OrderCancelledEvent() {
                        this.cancelEventType = assertMatchingCancelEventType("some_sender");
                    }
                    private String assertMatchingCancelEventType(String sender) {
                        return "CANCELLED_BY_SENDER";
                    }
                    public String getType() { return cancelEventType; }
                }
                
                class OrderService {
                    public void process(String event) {}
                }
                """;
        Path tempDir = Files.createTempDirectory("callgraph_dynamic_rhs");
        Files.writeString(tempDir.resolve("OrderConfig.java"), source);
        CodebaseContext context = new CodebaseContext();
        context.scan(tempDir);
        HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);

        EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build();
        TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
        List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
        
        org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
        org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).contains("CANCELLED_BY_SENDER");
    }

    @Test
    void shouldResolveConstantsFromFieldDeclarationsAndInitializers() throws IOException {
        String source = """
                package com.example;
                public class OrderController {
                    private OrderService service;
                    public void handleStatus1() {
                        service.process(new EventWithFieldDecl().getType());
                    }
                    public void handleStatus2() {
                        service.process(new EventWithInitBlock().getType());
                    }
                }
                
                class EventWithFieldDecl {
                    private String event = "FIELD_DECL";
                    public String getType() { return event; }
                }
                
                class EventWithInitBlock {
                    private String event;
                    {
                        event = "INIT_BLOCK";
                    }
                    public String getType() { return event; }
                }
                
                class OrderService {
                    public void process(String event) {}
                }
                """;
        Path tempDir = Files.createTempDirectory("callgraph_field_init");
        Files.writeString(tempDir.resolve("OrderConfig.java"), source);
        CodebaseContext context = new CodebaseContext();
        context.scan(tempDir);
        HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);

        EntryPoint ep1 = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus1").build();
        EntryPoint ep2 = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus2").build();
        TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
        List<CallChain> chains = builder.findChains(List.of(ep1, ep2), List.of(trigger));
        
        org.assertj.core.api.Assertions.assertThat(chains).hasSize(2);
        
        java.util.List<String> allEvents = new java.util.ArrayList<>();
        chains.forEach(c -> allEvents.addAll(c.getTriggerPoint().getPolymorphicEvents()));
        org.assertj.core.api.Assertions.assertThat(allEvents).containsExactlyInAnyOrder("FIELD_DECL", "INIT_BLOCK");
    }

    @Test
    void shouldFallbackToScraperForComplexInlineInstantiations() throws IOException {
        String source = """
                package com.example;
                public class OrderController {
                    private OrderService service;
                    public void handleStatus() {
                        // The AST tracer will trace EventWithConstructorArg.getType()
                        // It will find `this.type = typeArg`, which it cannot resolve because it's a parameter.
                        // The fallback scraper should catch "COMPLEX_INLINE_CONST".
                        service.process(new EventWithConstructorArg("COMPLEX_INLINE_CONST", 123).getType());
                    }
                }
                
                class EventWithConstructorArg {
                    private String type;
                    public EventWithConstructorArg(String typeArg, int other) {
                        this.type = typeArg;
                    }
                    public String getType() { return type; }
                }
                
                class OrderService {
                    public void process(String event) {}
                }
                """;
        Path tempDir = Files.createTempDirectory("callgraph_scraper");
        Files.writeString(tempDir.resolve("OrderConfig.java"), source);
        CodebaseContext context = new CodebaseContext();
        context.scan(tempDir);
        HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);

        EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build();
        TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
        List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
        
        org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
        org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).contains("COMPLEX_INLINE_CONST");
    }

    @Test
    void shouldResolveEventFromConstructorAssignment() throws IOException {
        String source = """
                package com.example;
                public class OrderController {
                    private OrderService service;
                    public void handleStatus() {
                        EventWithConstructorArg e = new EventWithConstructorArg("EVENT_FROM_CONSTRUCTOR", 123);
                        service.process(e.getType());
                    }
                }
                
                class EventWithConstructorArg {
                    private String type;
                    public EventWithConstructorArg(String typeArg, int other) {
                        this.type = typeArg;
                    }
                    public String getType() { return type; }
                }
                
                class OrderService {
                    public void process(String event) {}
                }
                """;
        Path tempDir = Files.createTempDirectory("callgraph_constructor_assignment");
        Files.writeString(tempDir.resolve("OrderConfig.java"), source);
        CodebaseContext context = new CodebaseContext();
        context.scan(tempDir);
        HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);

        EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build();
        TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
        List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
        
        org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
        org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("EVENT_FROM_CONSTRUCTOR");
    }

    @Test
    void shouldPrioritizeSetterOverConstructor() throws IOException {
        String source = """
                package com.example;
                public class OrderController {
                    private OrderService service;
                    public void handleStatus() {
                        EventWithConstructorArg e = new EventWithConstructorArg("EVENT_FROM_CONSTRUCTOR", 123);
                        e.setType("EVENT_FROM_SETTER");
                        service.process(e.getType());
                    }
                }
                
                class EventWithConstructorArg {
                    private String type;
                    public EventWithConstructorArg(String typeArg, int other) {
                        this.type = typeArg;
                    }
                    public void setType(String t) { this.type = t; }
                    public String getType() { return type; }
                }
                
                class OrderService {
                    public void process(String event) {}
                }
                """;
        Path tempDir = Files.createTempDirectory("callgraph_setter_priority");
        Files.writeString(tempDir.resolve("OrderConfig.java"), source);
        CodebaseContext context = new CodebaseContext();
        context.scan(tempDir);
        HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);

        EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build();
        TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
        List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
        
        org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
        org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("EVENT_FROM_SETTER");
    }

    @Test
    void shouldResolveEventFromFieldConstructorAssignment() throws IOException {
        String source = """
                package com.example;
                public class OrderController {
                    private OrderService service;
                    private EventWithConstructorArg e;
                    public OrderController() {
                        this.e = new EventWithConstructorArg("EVENT_FROM_FIELD", 123);
                    }
                    public void handleStatus() {
                        service.process(this.e.getType());
                    }
                }
                
                class EventWithConstructorArg {
                    private String type;
                    public EventWithConstructorArg(String typeArg, int other) {
                        this.type = typeArg;
                    }
                    public String getType() { return type; }
                }
                
                class OrderService {
                    public void process(String event) {}
                }
                """;
        Path tempDir = Files.createTempDirectory("callgraph_field_assignment");
        Files.writeString(tempDir.resolve("OrderConfig.java"), source);
        CodebaseContext context = new CodebaseContext();
        context.scan(tempDir);
        HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);

        EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleStatus").build();
        TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
        List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
        System.out.println("DEBUG CHAINS: " + (chains.isEmpty() ? "empty" : chains.get(0).getTriggerPoint().getPolymorphicEvents()));
        
        org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
        org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("EVENT_FROM_FIELD");
    }

    @Test
    void shouldResolveEventFromLombokDataAndAllArgsConstructor() throws IOException {
        String source = """
                package com.example;
                import lombok.Data;
                import lombok.AllArgsConstructor;
                public class OrderController {
                    private OrderService service;
                    public void handleLombok() {
                        LombokEvent e = new LombokEvent("LOMBOK_EVENT", 1);
                        service.process(e.getEvent());
                    }
                }
                
                class OrderService {
                    public void process(String event) {}
                }
                
                @Data
                @AllArgsConstructor
                class LombokEvent {
                    private String event;
                    private int id;
                }
                """;
        Path tempDir = Files.createTempDirectory("callgraph_lombok");
        Files.writeString(tempDir.resolve("OrderConfig.java"), source);
        CodebaseContext context = new CodebaseContext();
        context.scan(tempDir);
        HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);

        EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleLombok").build();
        TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
        List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
        
        org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
        org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("LOMBOK_EVENT");
    }

    @Test
    void shouldFailHeuristicAndRequireAccurateLombokMapping() throws IOException {
        String source = """
                package com.example;
                import lombok.Data;
                import lombok.AllArgsConstructor;
                public class OrderController {
                    private OrderService service;
                    public void handleLombok() {
                        MultiEvent e = new MultiEvent("CORRECT_EVENT", "WRONG_EVENT");
                        service.process(e.getEventType());
                    }
                }
                
                class OrderService {
                    public void process(String event) {}
                }
                
                @Data
                @AllArgsConstructor
                class MultiEvent {
                    private String eventType;
                    private String unrelatedString;
                }
                """;
        Path tempDir = Files.createTempDirectory("callgraph_lombok_heuristic_break");
        Files.writeString(tempDir.resolve("OrderConfig.java"), source);
        CodebaseContext context = new CodebaseContext();
        context.scan(tempDir);
        HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);

        EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handleLombok").build();
        TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
        List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
        
        org.assertj.core.api.Assertions.assertThat(chains).hasSize(1);
        org.assertj.core.api.Assertions.assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents())
            .containsExactly("CORRECT_EVENT"); // If heuristic rips both, it will fail because it contains WRONG_EVENT too!
    }

    @Test
    void shouldResolveEventsWhenFieldTransformerMethodMatchesSetterPatternButArgIsNonConstant() throws IOException {
        String source = """
                package com.example;
                public class OrderController {
                    private Transformer transformer;
                    public void processEvent(String dto) {
                        Event e = transformer.transform(dto);
                        machine.fire(e.getType());
                    }
                }

                interface Transformer {
                    Event transform(String input);
                }

                class TransformerImpl implements Transformer {
                    public Event transform(String input) {
                        return new Event("ORDER_DISPATCHED");
                    }
                }

                class Event {
                    private String type;
                    public Event(String type) { this.type = type; }
                    public String getType() { return type; }
                }

                class Machine {
                    public void fire(String event) {}
                }
                """;
        java.nio.file.Path tempDir = java.nio.file.Files.createTempDirectory("callgraph_test_transformer");
        java.nio.file.Files.writeString(tempDir.resolve("OrderConfig.java"), source);

        CodebaseContext context = new CodebaseContext();
        context.scan(tempDir);

        HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);

        EntryPoint entryPoint = EntryPoint.builder()
                .className("com.example.OrderController")
                .methodName("processEvent")
                .build();

        TriggerPoint trigger = TriggerPoint.builder()
                .className("com.example.Machine")
                .methodName("fire")
                .event("event")
                .build();

        List<CallChain> chains = engine.findChains(List.of(entryPoint), List.of(trigger));

        assertThat(chains).hasSize(1);
        // Must resolve through TransformerImpl.transform() → "ORDER_DISPATCHED",
        // NOT return empty polymorphicEvents due to early exit on the setter match.
        assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents())
                .containsExactlyInAnyOrder("ORDER_DISPATCHED");
    }

    @Test
    void shouldResolveEnumPassedThroughWrapperObjectAndGetter() throws IOException {
        String source = """
                package com.example;

                public class OrderController {
                    private OrderMapper mapper;
                    private StateMachine machine;

                    public void processOrder(String dto) {
                        OrderEvent event = mapper.toEvent(dto);
                        machine.fire(event.getType());
                    }
                }

                interface OrderMapper {
                    OrderEvent toEvent(String input);
                }

                class OrderMapperImpl implements OrderMapper {
                    public OrderEvent toEvent(String input) {
                        return new OrderEvent(States.PAYMENT_RECEIVED);
                    }
                }

                class OrderEvent {
                    private States type;
                    public OrderEvent(States type) { this.type = type; }
                    public States getType() { return type; }  // bare field, no this.
                }

                class StateMachine {
                    public void fire(States event) {}
                }

                enum States { PAYMENT_RECEIVED, CANCELLED }
                """;
        java.nio.file.Path tempDir = java.nio.file.Files.createTempDirectory("callgraph_test_wrapper");
        java.nio.file.Files.writeString(tempDir.resolve("OrderConfig.java"), source);

        CodebaseContext context = new CodebaseContext();
        context.scan(tempDir);

        HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);

        EntryPoint entryPoint = EntryPoint.builder()
                .className("com.example.OrderController")
                .methodName("processOrder")
                .build();

        TriggerPoint trigger = TriggerPoint.builder()
                .className("com.example.StateMachine")
                .methodName("fire")
                .event("event")
                .build();

        List<CallChain> chains = engine.findChains(List.of(entryPoint), List.of(trigger));

        assertThat(chains).hasSize(1);
        System.out.println("DIAGNOSTIC polymorphicEvents: " +
                chains.get(0).getTriggerPoint().getPolymorphicEvents());
        assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents())
                .containsExactlyInAnyOrder("States.PAYMENT_RECEIVED");
    }

    @Test
    void shouldResolveTransformingGetterThatMapsDtoEnumToStateMachineEnum() throws IOException {
        String source = """
                package com.example;

                public class OrderController {
                    private OrderMapper mapper;
                    private StateMachine machine;

                    public void processOrder(String dto) {
                        OrderMsg msg = mapper.toMsg(dto);
                        machine.fire(msg.getSmEvent());
                    }
                }

                interface OrderMapper {
                    OrderMsg toMsg(String input);
                }

                class OrderMapperImpl implements OrderMapper {
                    public OrderMsg toMsg(String input) {
                        return new OrderMsg(OrderType.PAY);
                    }
                }

                class OrderMsg {
                    private OrderType type;
                    public OrderMsg(OrderType type) { this.type = type; }
                    public SMEvent getSmEvent() {
                        return switch (type) {
                            case PAY    -> SMEvent.PAY_RECEIVED;
                            case CANCEL -> SMEvent.ORDER_CANCELLED;
                        };
                    }
                }

                class StateMachine {
                    public void fire(SMEvent event) {}
                }

                enum OrderType { PAY, CANCEL }
                enum SMEvent { PAY_RECEIVED, ORDER_CANCELLED }
                """;
        java.nio.file.Path tempDir = java.nio.file.Files.createTempDirectory("callgraph_test_twoenum");
        java.nio.file.Files.writeString(tempDir.resolve("OrderConfig.java"), source);

        CodebaseContext context = new CodebaseContext();
        context.scan(tempDir);

        HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);

        EntryPoint entryPoint = EntryPoint.builder()
                .className("com.example.OrderController")
                .methodName("processOrder")
                .build();

        TriggerPoint trigger = TriggerPoint.builder()
                .className("com.example.StateMachine")
                .methodName("fire")
                .event("event")
                .build();

        List<CallChain> chains = engine.findChains(List.of(entryPoint), List.of(trigger));

        assertThat(chains).hasSize(1);
        System.out.println("TWO-ENUM polymorphicEvents: " +
                chains.get(0).getTriggerPoint().getPolymorphicEvents());
        // Must return the SM enum, NOT the DTO enum (OrderType.PAY would be wrong)
        assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents())
                .containsExactlyInAnyOrder("SMEvent.PAY_RECEIVED");
    }

    @Test
    void shouldResolveGetterOnInterProceduralClassInstanceCreation() throws IOException {
        String source = """
                package com.example;
                public class OrderController {
                    private OrderService service;
                    public void handle() {
                        service.process(new DomainEvent("PAYLOAD_ID"));
                    }
                }
                
                class DomainEvent {
                    private String id;
                    public DomainEvent(String id) { this.id = id; }
                    public String getEvent() { return "INLINE_EVENT"; }
                }
                
                class OrderService {
                    public void process(DomainEvent event) {
                        sendEvent(event.getEvent());
                    }
                    public void sendEvent(String ev) {}
                }
                """;
        Path tempDir = Files.createTempDirectory("callgraph_interproc_cic");
        Files.writeString(tempDir.resolve("OrderConfig.java"), source);
        CodebaseContext context = new CodebaseContext();
        context.scan(tempDir);
        HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);

        EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderController").methodName("handle").build();
        TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("sendEvent").event("ev").build();
        List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
        
        assertThat(chains).hasSize(1);
        assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("INLINE_EVENT");
    }
}
