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.LinkedHashSet;
import java.util.List;
import java.util.Set;

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

/**
 * Adversarial regression for {@code IdentityStringTransformSupport} expand-hint wiring in ACE.
 * Locale arity expands here; EventIdentity peels stay 0-arg only
 * ({@code EventIdentityResolverPipelineTest#toUpperCaseWithLocaleStaysFailClosed}).
 */
class EnumExpansionHintBugHuntTest {

    @Test
    void valueOfActionToUpperCaseExpandsToEnumMembers(@TempDir Path tempDir) throws IOException {
        CallChain chain = resolveSendEvent(tempDir, """
                package com.example;
                public class ApiController {
                    StateMachine sm;
                    public void processAction(String action) {
                        sm.sendEvent(OrderEvent.valueOf(action.toUpperCase()));
                    }
                }
                enum OrderEvent { PAY, SHIP, CANCEL }
                class StateMachine { public void sendEvent(OrderEvent e) {} }
                """, "processAction");
        assertThat(chain.getTriggerPoint().getPolymorphicEvents())
                .contains("OrderEvent.PAY", "OrderEvent.SHIP", "OrderEvent.CANCEL");
    }

    @Test
    void valueOfActionToUpperCaseLocaleExpandsToEnumMembers(@TempDir Path tempDir) throws IOException {
        CallChain chain = resolveSendEvent(tempDir, """
                package com.example;
                import java.util.Locale;
                public class ApiController {
                    StateMachine sm;
                    public void processAction(String action) {
                        sm.sendEvent(OrderEvent.valueOf(action.toUpperCase(Locale.ROOT)));
                    }
                }
                enum OrderEvent { PAY, SHIP, CANCEL }
                class StateMachine { public void sendEvent(OrderEvent e) {} }
                """, "processAction");
        assertThat(chain.getTriggerPoint().getPolymorphicEvents())
                .contains("OrderEvent.PAY", "OrderEvent.SHIP", "OrderEvent.CANCEL");
    }

    @Test
    void valueOfActionToLowerCaseExpandsToEnumMembers(@TempDir Path tempDir) throws IOException {
        CallChain chain = resolveSendEvent(tempDir, """
                package com.example;
                public class ApiController {
                    StateMachine sm;
                    public void processAction(String action) {
                        sm.sendEvent(OrderEvent.valueOf(action.toLowerCase()));
                    }
                }
                enum OrderEvent { PAY, SHIP, CANCEL }
                class StateMachine { public void sendEvent(OrderEvent e) {} }
                """, "processAction");
        assertThat(chain.getTriggerPoint().getPolymorphicEvents())
                .contains("OrderEvent.PAY", "OrderEvent.SHIP", "OrderEvent.CANCEL");
    }

    @Test
    void valueOfViaStringToUpperCaseMethodReferenceExpands(@TempDir Path tempDir) throws IOException {
        CallChain chain = resolveSendEvent(tempDir, """
                package com.example;
                import java.util.Optional;
                public class ApiController {
                    StateMachine sm;
                    public void processAction(String action) {
                        sm.sendEvent(OrderEvent.valueOf(
                            Optional.of(action).map(String::toUpperCase).orElse(action)));
                    }
                }
                enum OrderEvent { PAY, SHIP, CANCEL }
                class StateMachine { public void sendEvent(OrderEvent e) {} }
                """, "processAction");
        assertThat(chain.getTriggerPoint().getPolymorphicEvents())
                .contains("OrderEvent.PAY", "OrderEvent.SHIP", "OrderEvent.CANCEL");
    }

    @Test
    void plainValueOfUnboundStaysSymbolic(@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);
        assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents())
                .containsExactly("<SYMBOLIC: com.example.MyEvents.*>");
    }

    @Test
    void helperValueOfNameFillsPolymorphicEvents(@TempDir Path tempDir) throws IOException {
        String source = """
                package com.example;
                public class LogisticsController {
                    private LogisticsService service;
                    public void handle(String event) {
                        service.process(event);
                    }
                }
                abstract class AbstractOrderService {
                    public void process(String event) {
                        String resolved = assertSupportedOrderEvent(event);
                        sendEvent(resolved);
                    }
                    protected abstract String assertSupportedOrderEvent(String event);
                    public void sendEvent(String ev) {}
                }
                class LogisticsService extends AbstractOrderService {
                    @Override
                    protected String assertSupportedOrderEvent(String event) {
                        return LogisticsEvent.valueOf(event).name();
                    }
                }
                class ComputerStoreService extends AbstractOrderService {
                    @Override
                    protected String assertSupportedOrderEvent(String event) {
                        return ComputerEvent.valueOf(event).name();
                    }
                }
                enum LogisticsEvent { DELIVERED, RETURNED }
                enum ComputerEvent { SHIPPED, REFUNDED }
                """;
        Files.writeString(tempDir.resolve("OrderConfig.java"), source);
        CodebaseContext context = new CodebaseContext();
        context.setResolveBindings(true);
        context.scan(tempDir);
        HeuristicCallGraphEngine builder = new HeuristicCallGraphEngine(context);
        EntryPoint entryPoint = EntryPoint.builder()
                .className("com.example.LogisticsController")
                .methodName("handle")
                .build();
        TriggerPoint trigger = TriggerPoint.builder()
                .className("com.example.AbstractOrderService")
                .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("LogisticsEvent.DELIVERED", "LogisticsEvent.RETURNED");
    }

    @Test
    void coincidentalNameInUnrelatedLocalMethodDoesNotFillEmptyPoly(@TempDir Path tempDir) throws IOException {
        CallChain chain = resolveSendEvent(tempDir, """
                package com.example;
                public class ApiController {
                    StateMachine sm;
                    public void process(String action) {
                        String ignored = debugLabel();
                        sm.sendEvent(OrderEvent.valueOf(action));
                    }
                    private String debugLabel() {
                        return OrderEvent.PAY.name();
                    }
                }
                enum OrderEvent { PAY, SHIP, CANCEL }
                class StateMachine { public void sendEvent(OrderEvent e) {} }
                """, "process");
        List<String> poly = chain.getTriggerPoint().getPolymorphicEvents();
        assertThat(poly)
                .as("unrelated .name() helper must not fill empty poly with concretes, got %s", poly)
                .noneMatch(e -> e != null && e.startsWith("OrderEvent.") && !e.contains("SYMBOLIC"));
    }

    @Test
    void noiseNameInsideMappedHelperDoesNotExpand(@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) {
                        String ignored = OrderEvent.PAY.name();
                        return MyEvents.valueOf(str);
                    }
                    public void sendEvent(MyEvents e) {}
                }
                enum MyEvents { A, B }
                enum OrderEvent { PAY }
                """;
        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);
        List<String> poly = chains.get(0).getTriggerPoint().getPolymorphicEvents();
        assertThat(poly)
                .as("noise .name() in helper body must not expand, got %s", poly)
                .noneMatch(e -> e != null && (e.equals("MyEvents.A") || e.equals("MyEvents.B")));
        assertThat(poly.stream().anyMatch(e -> e != null && e.contains("SYMBOLIC")))
                .as("expected symbolic poly, got %s", poly)
                .isTrue();
    }

    @Test
    void threeHopPlainValueOfStaysSymbolic(@TempDir Path tempDir) throws IOException {
        String source = """
                package com.example;
                public class Endpoint {
                    private Facade facade;
                    public void trigger(String eventString) {
                        facade.accept(eventString);
                    }
                }
                class Facade {
                    private OrderService service;
                    public void accept(String str) {
                        service.doSomething(str);
                    }
                }
                class OrderService {
                    public void doSomething(String str) {
                        MyEvents event = MyEvents.valueOf(str);
                        sendEvent(event);
                    }
                    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);
        List<String> poly = chains.get(0).getTriggerPoint().getPolymorphicEvents();
        assertThat(poly)
                .as("3-hop plain valueOf must stay symbolic, got %s", poly)
                .noneMatch(e -> e != null && (e.equals("MyEvents.A") || e.equals("MyEvents.B")));
        assertThat(poly.stream().anyMatch(e -> e != null && e.contains("SYMBOLIC")))
                .as("expected symbolic poly, got %s", poly)
                .isTrue();
    }

    @Test
    void multiTypeEnterpriseDispatcherStaysMultiSymbolic() 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(List.of(
                        EntryPoint.Parameter.builder()
                                .name("machineType")
                                .type("String")
                                .annotations(List.of("PathVariable"))
                                .build(),
                        EntryPoint.Parameter.builder()
                                .name("event")
                                .type("String")
                                .annotations(List.of("PathVariable"))
                                .build()))
                .build();
        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();
        Set<String> polymorphicEvents = new LinkedHashSet<>();
        for (CallChain chain : chains) {
            if (chain.getTriggerPoint().getPolymorphicEvents() != null) {
                polymorphicEvents.addAll(chain.getTriggerPoint().getPolymorphicEvents());
            }
        }
        assertThat(polymorphicEvents)
                .contains("<SYMBOLIC: OrderEvent.*>", "<SYMBOLIC: DocumentEvent.*>", "<SYMBOLIC: UserEvent.*>");
        assertThat(polymorphicEvents.stream()
                        .filter(e -> e != null && !e.contains("SYMBOLIC") && e.contains("Event."))
                        .toList())
                .as("must not flatten multi-type SYMBOLIC into concrete union: %s", polymorphicEvents)
                .isEmpty();
    }

    @Test
    void stringLiteralNameSubstringDoesNotFalseHint(@TempDir Path tempDir) throws IOException {
        CallChain chain = resolveSendEvent(tempDir, """
                package com.example;
                public class ApiController {
                    StateMachine sm;
                    public void process(String action) {
                        String note = ".name()";
                        sm.sendEvent(OrderEvent.valueOf(action));
                    }
                }
                enum OrderEvent { PAY, SHIP, CANCEL }
                class StateMachine { public void sendEvent(OrderEvent e) {} }
                """, "process");
        List<String> poly = chain.getTriggerPoint().getPolymorphicEvents();
        assertThat(poly)
                .as("string literal \".name()\" must not false-hint expand, got %s", poly)
                .noneMatch(e -> "OrderEvent.PAY".equals(e)
                        || "OrderEvent.SHIP".equals(e)
                        || "OrderEvent.CANCEL".equals(e));
    }

    private static CallChain resolveSendEvent(Path tempDir, String source, String entryMethod) throws IOException {
        Files.writeString(tempDir.resolve("ApiController.java"), source);
        CodebaseContext context = new CodebaseContext();
        context.scan(tempDir);
        HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
        EntryPoint entryPoint = EntryPoint.builder()
                .className("com.example.ApiController")
                .methodName(entryMethod)
                .build();
        TriggerPoint trigger = TriggerPoint.builder()
                .className("com.example.StateMachine")
                .methodName("sendEvent")
                .event("e")
                .build();
        List<CallChain> chains = engine.findChains(List.of(entryPoint), List.of(trigger));
        assertThat(chains).isNotEmpty();
        return chains.get(0);
    }
}
