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 HeuristicCallGraphEngineTypeTest {

    @Test
    void shouldResolveLocalSetterAndSwitchExpressionPolymorphism(@TempDir Path tempDir) throws IOException {
        String source = """
                package com.example;
                public class OrderService {
                    public void updateOrderState(String vv) {
                        StateMachine sm = new StateMachine();
                        MysteryPayload a = new MysteryPayload();
                        a.setType(my_func(vv));
                        sm.sendEvent(a.getType());
                    }
                    
                    private OrderEvents my_func(String q) {
                        return switch (q) {
                            case "a" -> OrderEvents.A8;
                            case "b" -> OrderEvents.A133;
                            default -> throw new IllegalArgumentException("Unknown");
                        };
                    }
                }
                
                enum OrderEvents { A8, A133 }
                
                class MysteryPayload {
                    private OrderEvents type;
                    public void setType(OrderEvents type) { this.type = type; }
                    public OrderEvents getType() { return type; }
                }
                
                class StateMachine {
                    public void sendEvent(OrderEvents event) {}
                }
                """;
        Files.writeString(tempDir.resolve("OrderService.java"), source);

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

        HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);

        EntryPoint entryPoint = EntryPoint.builder()
                .className("com.example.OrderService")
                .methodName("updateOrderState")
                .build();

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

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

        assertThat(chains).hasSize(1);
        CallChain chain = chains.get(0);
        
        System.out.println("Resolved poly events: " + chain.getTriggerPoint().getPolymorphicEvents());
        assertThat(chain.getTriggerPoint().getPolymorphicEvents())
                .containsExactlyInAnyOrder("OrderEvents.A8", "OrderEvents.A133");
    }

    @Test
    void shouldResolveOldStyleSwitchStatementPolymorphism(@TempDir Path tempDir) throws IOException {
        String source = """
                package com.example;
                public class OrderService {
                    public void updateOrderState(String vv) {
                        StateMachine sm = new StateMachine();
                        sm.sendEvent(getEventFromOldSwitch(vv));
                    }
                    
                    private OrderEvents getEventFromOldSwitch(String q) {
                        switch (q) {
                            case "a": return OrderEvents.A8;
                            case "b": 
                            case "c": return OrderEvents.A133;
                            default: return OrderEvents.PAY;
                        }
                    }
                }
                
                enum OrderEvents { A8, A133, PAY }
                
                class StateMachine {
                    public void sendEvent(OrderEvents event) {}
                }
                """;
        Files.writeString(tempDir.resolve("OrderService.java"), source);
        CodebaseContext context = new CodebaseContext();
        context.scan(tempDir);
        HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);

        EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderService").methodName("updateOrderState").build();
        TriggerPoint trigger = TriggerPoint.builder().className("com.example.StateMachine").methodName("sendEvent").event("event").build();
        List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
        assertThat(chains).hasSize(1);
        assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents())
                .containsExactlyInAnyOrder("OrderEvents.A8", "OrderEvents.A133", "OrderEvents.PAY");
    }

    @Test
    void shouldResolveMultiValueSwitchExpressionPolymorphism(@TempDir Path tempDir) throws IOException {
        String source = """
                package com.example;
                public class OrderService {
                    public void updateOrderState(String vv) {
                        StateMachine sm = new StateMachine();
                        sm.sendEvent(getEventMultiValue(vv));
                    }
                    
                    private OrderEvents getEventMultiValue(String q) {
                        return switch (q) {
                            case "a", "b" -> OrderEvents.A8;
                            case "c" -> OrderEvents.A133;
                            default -> OrderEvents.PAY;
                        };
                    }
                }
                
                enum OrderEvents { A8, A133, PAY }
                
                class StateMachine {
                    public void sendEvent(OrderEvents event) {}
                }
                """;
        Files.writeString(tempDir.resolve("OrderService.java"), source);
        CodebaseContext context = new CodebaseContext();
        context.scan(tempDir);
        HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);

        EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderService").methodName("updateOrderState").build();
        TriggerPoint trigger = TriggerPoint.builder().className("com.example.StateMachine").methodName("sendEvent").event("event").build();
        List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
        assertThat(chains).hasSize(1);
        assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents())
                .containsExactlyInAnyOrder("OrderEvents.A8", "OrderEvents.A133", "OrderEvents.PAY");
    }

    @Test
    void shouldResolveSwitchExpressionWithBlockAndYieldPolymorphism(@TempDir Path tempDir) throws IOException {
        String source = """
                package com.example;
                public class OrderService {
                    public void updateOrderState(String vv) {
                        StateMachine sm = new StateMachine();
                        sm.sendEvent(getEventWithYield(vv));
                    }
                    
                    private OrderEvents getEventWithYield(String q) {
                        return switch (q) {
                            case "a" -> {
                                System.out.println("Processing a");
                                yield OrderEvents.A8;
                            }
                            case "b" -> { yield OrderEvents.A133; }
                            default -> OrderEvents.PAY;
                        };
                    }
                }
                
                enum OrderEvents { A8, A133, PAY }
                
                class StateMachine {
                    public void sendEvent(OrderEvents event) {}
                }
                """;
        Files.writeString(tempDir.resolve("OrderService.java"), source);
        CodebaseContext context = new CodebaseContext();
        context.scan(tempDir);
        HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);

        EntryPoint entryPoint = EntryPoint.builder().className("com.example.OrderService").methodName("updateOrderState").build();
        TriggerPoint trigger = TriggerPoint.builder().className("com.example.StateMachine").methodName("sendEvent").event("event").build();
        List<CallChain> chains = builder.findChains(List.of(entryPoint), List.of(trigger));
        assertThat(chains).hasSize(1);
        assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents())
                .containsExactlyInAnyOrder("OrderEvents.A8", "OrderEvents.A133", "OrderEvents.PAY");
    }

    @Test
    void shouldTraceEventsThroughMapStructInterfaceImpl() throws IOException {
        String source = """
                package com.example;
                public class OrderController {
                    private OrderService service;
                    private OrderMapper mapper = new OrderMapperImpl();
                    public void processOrderEvent(String eventDtoStr) {
                        OrderEvent e = mapper.toEvent(eventDtoStr);
                        service.updateOrderState(e.getEvent());
                    }
                }
                
                class OrderService {
                    public void updateOrderState(String event) {}
                }
                
                interface OrderMapper {
                    OrderEvent toEvent(String source);
                }
                
                class OrderMapperImpl implements OrderMapper {
                    @Override
                    public OrderEvent toEvent(String source) {
                        return new OrderEvent("MAPPED_EVENT");
                    }
                }
                
                class OrderEvent {
                    private String event;
                    public OrderEvent(String event) { this.event = event; }
                    public String getEvent() { return event; }
                }
                """;
        Path tempDir = Files.createTempDirectory("callgraph_test_mapstruct");
        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("processOrderEvent")
                .build();

        TriggerPoint trigger = TriggerPoint.builder()
                .className("com.example.OrderService")
                .methodName("updateOrderState")
                .event("event")
                .build();

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

        assertThat(chains).hasSize(1);
        CallChain chain = chains.get(0);
        
        assertThat(chain.getTriggerPoint().getPolymorphicEvents()).containsExactlyInAnyOrder("MAPPED_EVENT");
    }

    @Test
    void shouldTraceArrayAndCollectionAccessGracefully() throws IOException {
        String source = """
                package com.example;
                public class OrderController {
                    private OrderService service;
                    public void processOrderEvent() {
                        OrderEvents[] events = new OrderEvents[]{OrderEvents.PAY};
                        service.updateOrderState(events[0]);
                    }
                }
                
                class OrderService {
                    public void updateOrderState(OrderEvents event) {}
                }
                
                enum OrderEvents { PAY }
                """;
        Path tempDir = Files.createTempDirectory("callgraph_test_array");
        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("processOrderEvent")
                .build();

        TriggerPoint trigger = TriggerPoint.builder()
                .className("com.example.OrderService")
                .methodName("updateOrderState")
                .event("event")
                .build();

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

        assertThat(chains).hasSize(1);
        CallChain chain = chains.get(0);
        // We don't dynamically resolve array indexes, but it shouldn't crash.
        // It returns the array access expression as a string.
        assertThat(chain.getTriggerPoint().getEvent()).isEqualTo("new OrderEvents[]{OrderEvents.PAY}[0]");
    }

    @Test
    void shouldResolveConstantsFromSwitchStatementInMethod() throws IOException {
        String source = """
                package com.example;
                public class OrderController {
                    private OrderService service;
                    public void handleStatus(DomainEvent domainEvent) {
                        service.process(domainEvent.getEventForStatus());
                    }
                }
                
                class DomainEvent {
                    private OrderStatus status;
                    public String getEventForStatus() {
                        switch (status) {
                            case NEW: return "CREATE_EVENT";
                            case PAID: return "PAY_EVENT";
                            default: return "CANCEL_EVENT";
                        }
                    }
                }
                
                class OrderService {
                    public void process(String event) {}
                }
                
                enum OrderStatus { NEW, PAID }
                """;
        Path tempDir = Files.createTempDirectory("callgraph_switch");
        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("CREATE_EVENT", "PAY_EVENT", "CANCEL_EVENT");
    }

    @Test
    void shouldResolveConstantsFromEnumReturn() throws IOException {
        String source = """
                package com.example;
                public class OrderController {
                    private OrderService service;
                    public void handleStatus(DomainEvent domainEvent) {
                        service.process(domainEvent.getEventEnum());
                    }
                }
                
                class DomainEvent {
                    public String getEventEnum() {
                        return MyEvents.ORDER_PAID.name();
                    }
                }
                
                class OrderService {
                    public void process(String event) {}
                }
                
                enum MyEvents { ORDER_PAID }
                """;
        Path tempDir = Files.createTempDirectory("callgraph_enum_return");
        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("ORDER_PAID");
    }

    @Test
    void shouldResolveEventFromArrayAndList() throws IOException {
        String source = """
                package com.example;
                import java.util.List;
                public class OrderController {
                    private OrderService service;
                    public void handleArray() {
                        String[] events = {"ARRAY_EVENT"};
                        service.process(events[0]);
                    }
                    public void handleList() {
                        List<String> listEvents = List.of("LIST_EVENT");
                        service.process(listEvents.get(0));
                    }
                }
                
                class OrderService {
                    public void process(String event) {}
                }
                """;
        Path tempDir = Files.createTempDirectory("callgraph_array");
        Files.writeString(tempDir.resolve("OrderConfig.java"), source);
        CodebaseContext context = new CodebaseContext();
        context.scan(tempDir);
        HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);

        EntryPoint entryPoint1 = EntryPoint.builder().className("com.example.OrderController").methodName("handleArray").build();
        EntryPoint entryPoint2 = EntryPoint.builder().className("com.example.OrderController").methodName("handleList").build();
        TriggerPoint trigger = TriggerPoint.builder().className("com.example.OrderService").methodName("process").event("event").build();
        List<CallChain> chains = builder.findChains(List.of(entryPoint1, entryPoint2), List.of(trigger));
        
        org.assertj.core.api.Assertions.assertThat(chains).hasSize(2);
        List<String> resolvedEvents = chains.stream().flatMap(c -> c.getTriggerPoint().getPolymorphicEvents().stream()).toList();
        org.assertj.core.api.Assertions.assertThat(resolvedEvents).containsExactlyInAnyOrder("ARRAY_EVENT", "LIST_EVENT");
    }

    @Test
    void shouldResolveEnumValueOfFromString(@TempDir Path tempDir) throws IOException {
        String source = """
                package com.example;
                public class Endpoint {
                    private OrderService service;
                    public void trigger(String eventString) {
                        service.doSomething(eventString);
                    }
                }
                
                class OrderService {
                    public void doSomething(String str) {
                        MyEvents event = mapToEnum(str);
                        sendEvent(event);
                    }
                    private MyEvents mapToEnum(String str) {
                        return MyEvents.valueOf(str);
                    }
                    public void sendEvent(MyEvents e) {}
                }
                
                enum MyEvents { A, B }
                """;
        Files.writeString(tempDir.resolve("Endpoint.java"), source);

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

        HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);

        EntryPoint entryPoint = EntryPoint.builder()
                .className("com.example.Endpoint")
                .methodName("trigger")
                .build();

        TriggerPoint trigger = TriggerPoint.builder()
                .className("com.example.OrderService")
                .methodName("sendEvent")
                .event("e")
                .build();

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

        assertThat(chains).hasSize(1);
        CallChain chain = chains.get(0);
        assertThat(chain.getTriggerPoint().getEvent()).isEqualTo("mapToEnum(str)");
        assertThat(chain.getTriggerPoint().getPolymorphicEvents())
            .containsExactly("<SYMBOLIC: com.example.MyEvents.*>");
    }

    @Test
    void shouldResolveTernaryValueOf(@TempDir Path tempDir) throws IOException {
        String source = """
                package com.example;
                public class Dispatcher {
                    public void dispatch(String type, String eventStr) {
                        StateMachine sm = new StateMachine();
                        if ("ORDER".equals(type)) {
                            OrderEvent ev = OrderEvent.valueOf(eventStr);
                            sm.sendEvent(ev);
                        } else {
                            UserEvent ev = UserEvent.valueOf(eventStr);
                            sm.sendEvent(ev);
                        }
                    }
                }
                enum OrderEvent { PAY }
                enum UserEvent { VERIFY }
                class StateMachine {
                    public void sendEvent(Object e) {}
                }
                """;
        Files.writeString(tempDir.resolve("Dispatcher.java"), source);
        CodebaseContext context = new CodebaseContext();
        context.scan(tempDir);

        HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
        
        EntryPoint entry = EntryPoint.builder()
                .className("com.example.Dispatcher")
                .methodName("dispatch")
                .build();
        TriggerPoint trigger = TriggerPoint.builder()
                .className("com.example.StateMachine")
                .methodName("sendEvent")
                .event("e")
                .build();
                
        List<CallChain> chains = builder.findChains(List.of(entry), List.of(trigger));
        assertThat(chains).hasSize(1);
        assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents())
                .containsExactlyInAnyOrder("<SYMBOLIC: OrderEvent.*>", "<SYMBOLIC: UserEvent.*>");
    }

    @Test
    void shouldResolveFullyQualifiedValueOfWithoutTruncatingPackage(@TempDir Path tempDir) throws IOException {
        // Regression: QualifiedName receivers must keep the type simple name.
        // Old bug stripped com.example.OrderEvent → com.example, yielding com.example.PAY.
        String source = """
                package com.example;
                public class Dispatcher {
                    public void dispatch() {
                        StateMachine sm = new StateMachine();
                        sm.sendEvent(com.example.OrderEvent.valueOf("PAY"));
                    }
                    public void dispatchVar(String event) {
                        StateMachine sm = new StateMachine();
                        sm.sendEvent(com.example.OrderEvent.valueOf(event));
                    }
                }
                enum OrderEvent { PAY, SHIP }
                class StateMachine {
                    public void sendEvent(Object e) {}
                }
                """;
        Files.writeString(tempDir.resolve("Dispatcher.java"), source);
        CodebaseContext context = new CodebaseContext();
        context.scan(tempDir);

        HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);

        EntryPoint literalEntry = EntryPoint.builder()
                .className("com.example.Dispatcher")
                .methodName("dispatch")
                .build();
        EntryPoint varEntry = EntryPoint.builder()
                .className("com.example.Dispatcher")
                .methodName("dispatchVar")
                .parameters(List.of(EntryPoint.Parameter.builder()
                        .name("event")
                        .type("String")
                        .build()))
                .build();
        TriggerPoint trigger = TriggerPoint.builder()
                .className("com.example.StateMachine")
                .methodName("sendEvent")
                .event("e")
                .build();

        List<CallChain> literalChains = builder.findChains(List.of(literalEntry), List.of(trigger));
        assertThat(literalChains).hasSize(1);
        assertThat(literalChains.get(0).getTriggerPoint().getPolymorphicEvents())
                .as("must keep OrderEvent type segment (not package-only com.example.PAY)")
                .isNotEmpty()
                .allSatisfy(e -> assertThat(e).contains("OrderEvent"))
                .noneMatch(e -> e.equals("com.example.PAY") || e.endsWith("."));

        List<CallChain> varChains = builder.findChains(List.of(varEntry), List.of(trigger));
        assertThat(varChains).isNotEmpty();
        assertThat(varChains.stream()
                        .flatMap(c -> c.getTriggerPoint().getPolymorphicEvents().stream())
                        .toList())
                .as("variable valueOf must not truncate FQN type to package prefix")
                .noneMatch(e -> e.equals("com.example.PAY") || e.startsWith("com.example.PAY") || e.endsWith("."));
    }

    @Test
    void testEnterpriseDispatcherResolution() throws IOException {
        CodebaseContext context = new CodebaseContext();
        context.scan(Path.of("../state_machines/state_machine_enterprise"));
        
        HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
        
        EntryPoint entry = EntryPoint.builder()
                .className("click.kamil.enterprise.web.StateMachineController")
                .methodName("transition")
                .name("POST /api/machine/{machineType}/transition/{event}")
                .parameters(java.util.List.of(
                        EntryPoint.Parameter.builder()
                                .name("machineType")
                                .type("String")
                                .annotations(java.util.List.of("PathVariable"))
                                .build(),
                        EntryPoint.Parameter.builder()
                                .name("event")
                                .type("String")
                                .annotations(java.util.List.of("PathVariable"))
                                .build()))
                .build();
        TriggerPoint trigger = TriggerPoint.builder()
                .className("click.kamil.enterprise.web.StateMachineDispatcher")
                .methodName("dispatch")
                .event("event")
                .build();
                
        List<CallChain> chains = builder.findChains(List.of(entry), List.of(trigger));
        assertThat(chains).isNotEmpty();
        java.util.Set<String> polymorphicEvents = new java.util.LinkedHashSet<>();
        for (CallChain chain : chains) {
            if (chain.getTriggerPoint().getPolymorphicEvents() != null) {
                polymorphicEvents.addAll(chain.getTriggerPoint().getPolymorphicEvents());
            }
            assertThat(chain.getTriggerPoint().getEvent())
                    .doesNotContain("true ?");
        }
        assertThat(polymorphicEvents)
                .contains("<SYMBOLIC: OrderEvent.*>", "<SYMBOLIC: DocumentEvent.*>", "<SYMBOLIC: UserEvent.*>");
    }
}
