package click.kamil.springstatemachineexporter.analysis.resolver;

import click.kamil.springstatemachineexporter.exporter.JsonExporter;
import click.kamil.springstatemachineexporter.service.ExportService;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;

import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

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

/**
 * End-to-end regression mirroring enterprise enums: boolean ctor arg + {@code isEvent()} guard
 * on dynamic dispatch must not link every enum constant to every transition.
 */
class EnterpriseBooleanEnumPredicateExportTest {

    @Test
    void exportShouldNotLinkAllTransitionsWhenEnumHasNonEventConstants(@TempDir Path tempDir) throws Exception {
        writeEnterpriseStyleProject(tempDir);

        Path outputDir = tempDir.resolve("out");
        ExportService exportService = new ExportService(List.of(new JsonExporter()));
        exportService.runExporter(
                tempDir,
                outputDir,
                List.of("json"),
                true,
                List.of(),
                null,
                "OrderStateMachineConfiguration",
                click.kamil.springstatemachineexporter.exporter.EnumFormat.fn,
                click.kamil.springstatemachineexporter.exporter.EnumFormat.fn);

        Path jsonFile = outputDir.resolve(
                "com.example.config.OrderStateMachineConfiguration/com.example.config.OrderStateMachineConfiguration.json");
        assertThat(jsonFile).exists();

        JsonNode root = new ObjectMapper().readTree(jsonFile.toFile());
        int transitionCount = root.get("transitions").size();
        assertThat(transitionCount).isEqualTo(2);

        List<JsonNode> callChains = StreamSupport.stream(root.get("metadata").get("callChains").spliterator(), false)
                .toList();
        assertThat(callChains).isNotEmpty();

        List<JsonNode> dispatchChains = callChains.stream()
                .filter(chain -> chain.get("methodChain").toString().contains("OrderDispatcher.dispatch"))
                .toList();
        assertThat(dispatchChains).isNotEmpty();

        List<String> polyEvents = dispatchChains.stream()
                .flatMap(chain -> StreamSupport.stream(
                        chain.get("triggerPoint").get("polymorphicEvents").spliterator(), false))
                .map(JsonNode::asText)
                .distinct()
                .toList();

        assertThat(polyEvents)
                .as("polymorphicEvents must exclude non-event enum constants")
                .doesNotContain(
                        "com.example.order.OrderEvent.LOG",
                        "com.example.order.OrderEvent.META")
                .contains(
                        "com.example.order.OrderEvent.PAY",
                        "com.example.order.OrderEvent.SHIP");
        assertThat(polyEvents.size()).isLessThan(4);

        for (JsonNode dispatchChain : dispatchChains) {
            JsonNode matched = dispatchChain.get("matchedTransitions");
            String resolution = dispatchChain.path("linkResolution").asText();
            // Wide predicate-filtered poly without per-endpoint dataflow must not
            // highlight every transition — fail closed instead.
            if ("AMBIGUOUS_WIDEN".equals(resolution) || "UNRESOLVED_EXTERNAL".equals(resolution)) {
                assertThat(matched == null || matched.isNull() || (matched.isArray() && matched.isEmpty()))
                        .as("fail-closed/unresolved chains must not carry matches")
                        .isTrue();
                continue;
            }
            long chainMatched = matched == null || matched.isNull()
                    ? 0
                    : StreamSupport.stream(matched.spliterator(), false).count();
            assertThat(chainMatched)
                    .as("each resolved dispatch chain must not cover all transitions without evidence")
                    .isLessThanOrEqualTo(1);
        }
        Set<String> matchedEvents = dispatchChains.stream()
                .filter(chain -> "RESOLVED".equals(chain.path("linkResolution").asText()))
                .flatMap(chain -> {
                    JsonNode matched = chain.get("matchedTransitions");
                    if (matched == null || matched.isNull() || !matched.isArray()) {
                        return Stream.empty();
                    }
                    return StreamSupport.stream(matched.spliterator(), false);
                })
                .map(node -> node.get("event").asText())
                .collect(Collectors.toSet());
        assertThat(matchedEvents)
                .doesNotContain("com.example.order.OrderEvent.LOG", "com.example.order.OrderEvent.META");
    }

    private static void writeEnterpriseStyleProject(Path projectRoot) throws Exception {
        Path javaRoot = projectRoot.resolve("src/main/java");
        Files.createDirectories(javaRoot.resolve("com/example/core"));
        Files.createDirectories(javaRoot.resolve("com/example/order"));
        Files.createDirectories(javaRoot.resolve("com/example/config"));
        Files.createDirectories(javaRoot.resolve("com/example/web"));
        Files.writeString(projectRoot.resolve("build.gradle"), "plugins { id 'java' }");

        Files.writeString(javaRoot.resolve("com/example/core/TriggerClassifier.java"), """
                package com.example.core;
                public interface TriggerClassifier {
                    boolean isEvent();
                }
                """);

        Files.writeString(javaRoot.resolve("com/example/order/OrderState.java"), """
                package com.example.order;
                public enum OrderState { NEW, PENDING, SHIPPED }
                """);

        Files.writeString(javaRoot.resolve("com/example/order/OrderEvent.java"), """
                package com.example.order;
                import com.example.core.TriggerClassifier;
                public enum OrderEvent implements TriggerClassifier {
                    PAY(true),
                    SHIP(true),
                    LOG(false),
                    META(false);
                    private final boolean event;
                    OrderEvent(boolean event) { this.event = event; }
                    @Override
                    public boolean isEvent() { return this.event; }
                }
                """);

        Files.writeString(javaRoot.resolve("com/example/config/OrderStateMachineConfiguration.java"), """
                package com.example.config;
                import com.example.order.OrderEvent;
                import com.example.order.OrderState;
                import org.springframework.context.annotation.Configuration;
                import org.springframework.statemachine.config.EnableStateMachineFactory;
                import org.springframework.statemachine.config.EnumStateMachineConfigurerAdapter;
                @Configuration
                @EnableStateMachineFactory(name = "orderStateMachineFactory")
                public class OrderStateMachineConfiguration
                        extends EnumStateMachineConfigurerAdapter<OrderState, OrderEvent> {
                    @Override
                    public void configure(
                            org.springframework.statemachine.config.builders.StateMachineStateConfigurer<OrderState, OrderEvent> states)
                            throws Exception {
                        states.withStates().initial(OrderState.NEW).state(OrderState.PENDING).end(OrderState.SHIPPED);
                    }
                    @Override
                    public void configure(
                            org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer<OrderState, OrderEvent> transitions)
                            throws Exception {
                        transitions.withExternal().source(OrderState.NEW).target(OrderState.PENDING).event(OrderEvent.PAY)
                                .and().withExternal().source(OrderState.PENDING).target(OrderState.SHIPPED).event(OrderEvent.SHIP);
                    }
                }
                """);

        Files.writeString(javaRoot.resolve("com/example/web/OrderDispatcher.java"), """
                package com.example.web;
                import com.example.order.OrderEvent;
                import com.example.order.OrderState;
                import org.springframework.beans.factory.annotation.Autowired;
                import org.springframework.statemachine.StateMachine;
                import org.springframework.statemachine.config.StateMachineFactory;
                import org.springframework.stereotype.Service;
                @Service
                public class OrderDispatcher {
                    @Autowired
                    private StateMachineFactory<OrderState, OrderEvent> orderStateMachineFactory;
                    public String dispatch(String eventString) {
                        OrderEvent eventType = OrderEvent.valueOf(eventString.toUpperCase());
                        if (eventType.isEvent()) {
                            StateMachine<OrderState, OrderEvent> machine = orderStateMachineFactory.getStateMachine();
                            machine.start();
                            machine.sendEvent(eventType);
                            return "Triggered " + eventType;
                        }
                        return "Ignored " + eventType;
                    }
                }
                """);

        Files.writeString(javaRoot.resolve("com/example/web/OrderController.java"), """
                package com.example.web;
                import org.springframework.beans.factory.annotation.Autowired;
                import org.springframework.web.bind.annotation.PathVariable;
                import org.springframework.web.bind.annotation.PostMapping;
                import org.springframework.web.bind.annotation.RestController;
                @RestController
                public class OrderController {
                    @Autowired
                    private OrderDispatcher orderDispatcher;
                    @PostMapping("/api/order/{event}")
                    public void transition(@PathVariable("event") String event) {
                        orderDispatcher.dispatch(event);
                    }
                }
                """);
    }
}
