package click.kamil.springstatemachineexporter.analysis.service;

import click.kamil.springstatemachineexporter.analysis.enricher.CallChainEnricher;
import click.kamil.springstatemachineexporter.analysis.enricher.EntryPointEnricher;
import click.kamil.springstatemachineexporter.analysis.enricher.TransitionLinkerEnricher;
import click.kamil.springstatemachineexporter.analysis.enricher.TriggerEnricher;
import click.kamil.springstatemachineexporter.analysis.model.AnalysisResult;
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.app.StateMachineAggregator;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import click.kamil.springstatemachineexporter.model.Transition;
import org.eclipse.jdt.core.dom.TypeDeclaration;
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.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

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

/**
 * Guards the shared analysis caches used when one codebase produces many inherited state machines.
 *
 * <p>Typical repo shape: one abstract config, several abstract branches, many concrete configs.
 * The exporter scans once, caches the call graph, then enriches each machine from the same context.
 */
class AnalysisCacheCorrectnessTest {

    private static Path findProjectRoot() {
        Path current = Path.of(".").toAbsolutePath();
        while (current != null && !Files.exists(current.resolve("settings.gradle"))) {
            current = current.getParent();
        }
        return current;
    }

    /**
     * The call graph is expensive to build; it must be stored on {@link CodebaseContext} and reused
     * by subsequent engines pointing at the same scan result.
     */
    @Test
    void heuristicCallGraphShouldBeCachedOnContext(@TempDir Path tempDir) throws Exception {
        String source = """
            package com.example;
            class A { void go() { new B().run("PAY"); } }
            class B { void run(String s) { new C().fire(com.example.E.PAY); } }
            enum E { PAY, SHIP }
            class C { void fire(E e) {} }
            """;
        Files.writeString(tempDir.resolve("App.java"), source);
        CodebaseContext context = new CodebaseContext();
        context.scan(tempDir);

        HeuristicCallGraphEngine firstEngine = new HeuristicCallGraphEngine(context);
        firstEngine.findChains(
                List.of(EntryPoint.builder().className("com.example.A").methodName("go").build()),
                List.of(TriggerPoint.builder().className("com.example.C").methodName("fire").event("e").build()));

        assertThat(context.getCache()).containsKey("heuristicCallGraph");
        Object cachedGraph = context.getCache().get("heuristicCallGraph");

        HeuristicCallGraphEngine secondEngine = new HeuristicCallGraphEngine(context);
        secondEngine.findChains(
                List.of(EntryPoint.builder().className("com.example.A").methodName("go").build()),
                List.of(TriggerPoint.builder().className("com.example.C").methodName("fire").event("e").build()));

        assertThat(context.getCache().get("heuristicCallGraph")).isSameAs(cachedGraph);
    }

    @Test
    void jdtIntelligenceProviderShouldCacheCallChains(@TempDir Path tempDir) throws Exception {
        String source = """
            package com.example;
            class A { void go() { new B().run("PAY"); } }
            class B { void run(String s) { new C().fire(com.example.E.PAY); } }
            enum E { PAY, SHIP }
            class C { void fire(E e) {} }
            """;
        Files.writeString(tempDir.resolve("App.java"), source);
        CodebaseContext context = new CodebaseContext();
        context.setResolveBindings(true);
        context.scan(tempDir);

        JdtIntelligenceProvider intelligence = new JdtIntelligenceProvider(context, tempDir);
        EntryPoint entry = EntryPoint.builder().className("com.example.A").methodName("go").build();
        TriggerPoint trigger = TriggerPoint.builder().className("com.example.C").methodName("fire").event("e").build();
        List<EntryPoint> entryPoints = List.of(entry);
        List<TriggerPoint> triggers = List.of(trigger);

        List<CallChain> first = intelligence.findCallChains(entryPoints, triggers);
        List<CallChain> second = intelligence.findCallChains(entryPoints, triggers);

        assertThat(second).isEqualTo(first);
    }

    /**
     * Two consecutive {@link CallGraphEngine#findChains} calls on a cached graph must agree.
     */
    @Test
    void repeatedCallChainResolutionShouldBeDeterministic(@TempDir Path tempDir) throws Exception {
        String source = """
            package com.example;
            class Controller {
                Dispatcher dispatcher;
                public void pay() { dispatcher.dispatch("ORDER", "PAY"); }
            }
            class Dispatcher {
                public void dispatch(String type, String action) {
                    StateMachine sm = new StateMachine();
                    if ("ORDER".equals(type)) {
                        OrderEvent ev = OrderEvent.valueOf(action);
                        sm.sendEvent(ev);
                    }
                }
            }
            enum OrderEvent { PAY, SHIP }
            class StateMachine { void sendEvent(OrderEvent e) {} }
            """;
        Files.writeString(tempDir.resolve("App.java"), source);
        CodebaseContext context = new CodebaseContext();
        context.scan(tempDir);

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

        HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
        CallChain first = engine.findChains(List.of(pay), List.of(trigger)).get(0);
        CallChain second = engine.findChains(List.of(pay), List.of(trigger)).get(0);

        assertThat(second.getTriggerPoint().getPolymorphicEvents())
                .isEqualTo(first.getTriggerPoint().getPolymorphicEvents());
        assertThat(second.getMethodChain()).isEqualTo(first.getMethodChain());
    }

    /**
     * A single {@link TransitionLinkerEnricher} instance is reused across machines during export.
     * Its internal memoization must not let machine B overwrite machine A's matched transitions.
     */
    @Test
    void sharedTransitionLinkerShouldNotCrossContaminateMachineResults(@TempDir Path tempDir) throws Exception {
        String source = """
            package com.example;
            class OrderController {
                SharedBus bus;
                void pay() { bus.orderPay(); }
            }
            class ShipmentController {
                SharedBus bus;
                void go() { bus.shipmentDispatch(); }
            }
            class SharedBus {
                public void orderPay() {
                    OrderMachine sm = new OrderMachine();
                    sm.sendEvent(OrderEvent.PAY);
                }
                public void shipmentDispatch() {
                    ShipmentMachine sm = new ShipmentMachine();
                    sm.sendEvent(ShipmentEvent.DISPATCH);
                }
            }
            enum OrderEvent { PAY }
            enum ShipmentEvent { DISPATCH }
            class OrderMachine { void sendEvent(OrderEvent e) {} }
            class ShipmentMachine { void sendEvent(ShipmentEvent e) {} }
            """;
        Files.writeString(tempDir.resolve("App.java"), source);
        CodebaseContext context = new CodebaseContext();
        context.scan(tempDir);
        HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);

        CallChain orderChain = engine.findChains(
                List.of(EntryPoint.builder().className("com.example.OrderController").methodName("pay").build()),
                List.of(TriggerPoint.builder().className("com.example.OrderMachine").methodName("sendEvent").event("e").build())
        ).get(0);
        CallChain shipmentChain = engine.findChains(
                List.of(EntryPoint.builder().className("com.example.ShipmentController").methodName("go").build()),
                List.of(TriggerPoint.builder().className("com.example.ShipmentMachine").methodName("sendEvent").event("e").build())
        ).get(0);

        TransitionLinkerEnricher sharedLinker = new TransitionLinkerEnricher();

        AnalysisResult orderMachine = analysisWithChain(
                "com.example.OrderStateMachineConfig",
                orderChain,
                CentralDispatcherTestSupport.transition("NEW", "PAID", "OrderEvent.PAY"));
        AnalysisResult shipmentMachine = analysisWithChain(
                "com.example.ShipmentStateMachineConfig",
                shipmentChain,
                CentralDispatcherTestSupport.transition("READY", "IN_TRANSIT", "ShipmentEvent.DISPATCH"));

        sharedLinker.enrich(orderMachine, null, null);
        sharedLinker.enrich(shipmentMachine, null, null);

        assertThat(orderMachine.getMetadata().getCallChains().get(0).getMatchedTransitions())
                .extracting("event")
                .containsExactly("OrderEvent.PAY");
        assertThat(shipmentMachine.getMetadata().getCallChains().get(0).getMatchedTransitions())
                .extracting("event")
                .containsExactly("ShipmentEvent.DISPATCH");

        // Re-enrich order machine after shipment machine was processed; result must stay stable.
        sharedLinker.enrich(orderMachine, null, null);
        assertThat(orderMachine.getMetadata().getCallChains().get(0).getMatchedTransitions())
                .extracting("event")
                .containsExactly("OrderEvent.PAY");
    }

    /**
     * Exercises a real inherited hierarchy (F1/F2/G1/G2) from one scan + one intelligence provider,
     * mirroring repos where many concrete configs extend shared abstract layers.
     */
    @Test
    void inheritedHierarchyShouldProduceStableDistinctMachineSnapshots() throws Exception {
        Path sampleRoot = findProjectRoot().resolve("state_machines/inheritance_extra_functions3_state_machine");
        CodebaseContext context = new CodebaseContext();
        context.setProjectRoot(sampleRoot);
        context.setSourcepath(List.of(sampleRoot.resolve("src/main/java").toString()));
        context.setResolveBindings(true);
        context.scan(sampleRoot);

        JdtIntelligenceProvider intelligence = new JdtIntelligenceProvider(context, sampleRoot);
        EnrichmentService enrichmentService = new EnrichmentService(List.of(
                new TriggerEnricher(),
                new EntryPointEnricher(),
                new CallChainEnricher(),
                new click.kamil.springstatemachineexporter.analysis.enricher.TriggerCanonicalizationEnricher(),
                new TransitionLinkerEnricher()));

        List<String> configNames = List.of(
                "F1StateMachineConfiguration",
                "F2StateMachineConfiguration",
                "G1StateMachineConfiguration",
                "G2StateMachineConfiguration");

        Map<String, Snapshot> firstPass = enrichAllConfigs(context, intelligence, enrichmentService, configNames);
        Map<String, Snapshot> secondPass = enrichAllConfigs(context, intelligence, enrichmentService, configNames);

        assertThat(secondPass).isEqualTo(firstPass);
        assertThat(firstPass).hasSize(4);

        List<Snapshot> snapshots = new ArrayList<>(firstPass.values());
        assertThat(snapshots.get(0).transitionCount()).isNotEqualTo(snapshots.get(2).transitionCount());
        assertThat(snapshots.get(0).eventNames()).isNotEqualTo(snapshots.get(2).eventNames());
    }

    /**
     * Synthetic repo shape: 1 root abstract → 3 branch abstracts → 4 concrete configs each (12 machines).
     * One scan and one intelligence provider must yield stable, distinct snapshots on repeat enrichment.
     *
     * <pre>
     * RootAbstractSmConfig
     * ├── BranchAAbstractSmConfig → BranchA1..A4SmConfig
     * ├── BranchBAbstractSmConfig → BranchB1..B4SmConfig
     * └── BranchCAbstractSmConfig → BranchC1..C4SmConfig
     * </pre>
     */
    @Test
    void twelveConfigSyntheticHierarchyShouldProduceStableDistinctSnapshots(@TempDir Path tempDir) throws Exception {
        TwelveConfigHierarchyTestSupport.writeSources(tempDir);

        CodebaseContext context = new CodebaseContext();
        context.setProjectRoot(tempDir);
        context.setResolveBindings(true);
        context.scan(tempDir);

        JdtIntelligenceProvider intelligence = new JdtIntelligenceProvider(context, tempDir);
        EnrichmentService enrichmentService = new EnrichmentService(List.of(
                new TriggerEnricher(),
                new EntryPointEnricher(),
                new CallChainEnricher(),
                new click.kamil.springstatemachineexporter.analysis.enricher.TriggerCanonicalizationEnricher(),
                new TransitionLinkerEnricher()));

        List<String> configNames = TwelveConfigHierarchyTestSupport.concreteConfigNames();

        Map<String, Snapshot> firstPass = enrichAllConfigs(context, intelligence, enrichmentService, configNames);
        Object cachedGraphAfterFirstPass = context.getCache().get("jdtCallGraph");

        Map<String, Snapshot> secondPass = enrichAllConfigs(context, intelligence, enrichmentService, configNames);

        assertThat(firstPass).hasSize(12);
        assertThat(secondPass).isEqualTo(firstPass);
        assertThat(new ArrayList<>(firstPass.values())).doesNotHaveDuplicates();

        // Each concrete config inherits root + branch + its own transition.
        assertThat(firstPass.get("com.example.hierarchy.BranchA1SmConfig").transitionCount()).isEqualTo(3);
        assertThat(firstPass.get("com.example.hierarchy.BranchA1SmConfig").eventNames())
                .containsExactlyInAnyOrderElementsOf(TwelveConfigHierarchyTestSupport.expectedEventsForConcrete("A", 1));
        assertThat(firstPass.get("com.example.hierarchy.BranchC4SmConfig").eventNames())
                .containsExactlyInAnyOrderElementsOf(TwelveConfigHierarchyTestSupport.expectedEventsForConcrete("C", 4));

        // Branch A and branch C machines must differ even when they share the same index.
        assertThat(firstPass.get("com.example.hierarchy.BranchA2SmConfig"))
                .isNotEqualTo(firstPass.get("com.example.hierarchy.BranchC2SmConfig"));

        // Call-graph cache must survive enriching all twelve configs (same graph instance on second pass).
        if (cachedGraphAfterFirstPass != null) {
            assertThat(context.getCache().get("jdtCallGraph")).isSameAs(cachedGraphAfterFirstPass);
        }
    }

    private static AnalysisResult analysisWithChain(String machineName, CallChain chain, Transition transition) {
        return AnalysisResult.builder()
                .name(machineName)
                .transitions(List.of(transition))
                .metadata(click.kamil.springstatemachineexporter.analysis.model.CodebaseMetadata.builder()
                        .callChains(List.of(chain))
                        .build())
                .build();
    }

    private static Map<String, Snapshot> enrichAllConfigs(
            CodebaseContext context,
            JdtIntelligenceProvider intelligence,
            EnrichmentService enrichmentService,
            List<String> configNames) {
        Map<String, Snapshot> snapshots = new LinkedHashMap<>();
        StateMachineAggregator aggregator = new StateMachineAggregator(context);

        for (String configName : configNames) {
            TypeDeclaration configType = context.getTypeDeclaration(configName);
            if (configType == null) {
                configType = context.getTypeDeclarations().stream()
                        .filter(td -> td.getName().getIdentifier().equals(configName))
                        .findFirst()
                        .orElse(null);
            }
            assertThat(configType).as("config type %s", configName).isNotNull();

            java.util.List<Transition> transitions = aggregator.aggregateTransitions(configType);
            AnalysisResult result = AnalysisResult.builder()
                    .name(configName)
                    .transitions(transitions)
                    .build();

            enrichmentService.enrich(result, context, intelligence);
            snapshots.put(context.getFqn(configType), Snapshot.from(result));
        }
        return snapshots;
    }

    private record Snapshot(int transitionCount, List<String> eventNames) {
        static Snapshot from(AnalysisResult result) {
            List<String> events = result.getTransitions().stream()
                    .map(t -> t.getEvent() != null ? t.getEvent().rawName() : null)
                    .filter(java.util.Objects::nonNull)
                    .map(Snapshot::stripLiteralQuotes)
                    .sorted()
                    .collect(Collectors.toList());
            return new Snapshot(result.getTransitions().size(), events);
        }

        private static String stripLiteralQuotes(String name) {
            if (name.length() >= 2 && name.startsWith("\"") && name.endsWith("\"")) {
                return name.substring(1, name.length() - 1);
            }
            return name;
        }
    }
}
