package click.kamil.springstatemachineexporter.analysis.service;

import click.kamil.springstatemachineexporter.analysis.enricher.TransitionLinkerEnricher;
import click.kamil.springstatemachineexporter.analysis.model.AnalysisResult;
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
import click.kamil.springstatemachineexporter.analysis.model.CodebaseMetadata;
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
import click.kamil.springstatemachineexporter.analysis.model.MatchedTransition;
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
import click.kamil.springstatemachineexporter.analysis.resolver.MachineEnumCanonicalizer;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import click.kamil.springstatemachineexporter.model.Event;
import click.kamil.springstatemachineexporter.model.State;
import click.kamil.springstatemachineexporter.model.Transition;

import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;

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

/**
 * Small helpers so dispatcher tests read as documentation of expected endpoint → transition behaviour.
 */
final class CentralDispatcherTestSupport {

    private CentralDispatcherTestSupport() {
    }

    enum EngineKind {
        HEURISTIC,
        JDT
    }

    static CodebaseContext scanSource(String source, Path tempDir) throws Exception {
        Files.writeString(tempDir.resolve("App.java"), source);
        CodebaseContext context = new CodebaseContext();
        context.scan(tempDir);
        return context;
    }

    static CallChain resolveChain(
            CodebaseContext context,
            String controllerClass,
            String controllerMethod,
            String stateMachineClass) {
        return resolveChain(context, controllerClass, controllerMethod, stateMachineClass, "sendEvent", EngineKind.HEURISTIC);
    }

    static CallChain resolveChain(
            CodebaseContext context,
            String controllerClass,
            String controllerMethod,
            String stateMachineClass,
            EngineKind engineKind) {
        return resolveChain(context, controllerClass, controllerMethod, stateMachineClass, "sendEvent", engineKind);
    }

    static CallChain resolveChain(
            CodebaseContext context,
            String entryClass,
            String entryMethod,
            String triggerClass,
            String triggerMethod,
            EngineKind engineKind) {
        AbstractCallGraphEngine engine = engineKind == EngineKind.JDT
                ? new JdtCallGraphEngine(context, null)
                : new HeuristicCallGraphEngine(context);
        EntryPoint entryPoint = EntryPoint.builder()
                .className(entryClass)
                .methodName(entryMethod)
                .build();
        TriggerPoint trigger = TriggerPoint.builder()
                .className(triggerClass)
                .methodName(triggerMethod)
                .event("e")
                .build();
        List<CallChain> chains = engine.findChains(List.of(entryPoint), List.of(trigger));
        assertThat(chains)
                .as("expected one call chain from %s.%s to %s.%s via %s",
                        entryClass, entryMethod, triggerClass, triggerMethod, engineKind)
                .hasSize(1);
        return chains.get(0);
    }

    static void assertParity(
            CodebaseContext context,
            String entryClass,
            String entryMethod,
            String triggerClass,
            String triggerMethod) {
        CallChain heuristic = resolveChain(
                context, entryClass, entryMethod, triggerClass, triggerMethod, EngineKind.HEURISTIC);
        CallChain jdt = resolveChain(
                context, entryClass, entryMethod, triggerClass, triggerMethod, EngineKind.JDT);
        assertThat(jdt.getMethodChain())
                .containsExactlyElementsOf(heuristic.getMethodChain());
        assertThat(jdt.getTriggerPoint().getEvent()).isEqualTo(heuristic.getTriggerPoint().getEvent());
        assertThat(jdt.getTriggerPoint().getPolymorphicEvents())
                .containsExactlyInAnyOrderElementsOf(
                        heuristic.getTriggerPoint().getPolymorphicEvents() == null
                                ? List.of()
                                : heuristic.getTriggerPoint().getPolymorphicEvents());
    }

    static void assertMethodChainContains(CallChain chain, String... methodFqns) {
        assertThat(chain.getMethodChain()).contains(methodFqns);
    }

    static void assertMethodChainContainsExact(CallChain chain, String... methodFqns) {
        assertThat(chain.getMethodChain()).containsExactly(methodFqns);
    }

    static void assertPolyEvents(CallChain chain, String... expectedEvents) {
        assertThat(chain.getTriggerPoint().getPolymorphicEvents())
                .containsExactlyInAnyOrder(expectedEvents);
    }

    static Transition transition(String source, String target, String eventFqn) {
        Transition transition = new Transition();
        transition.setSourceStates(List.of(State.of(source, source)));
        transition.setTargetStates(List.of(State.of(target, target)));
        transition.setEvent(Event.of(eventFqn, eventFqn));
        return transition;
    }

    static MatchedTransition linkEndpointToTransition(
            CallChain chain,
            String machineConfigName,
            Transition... machineTransitions) {
        AnalysisResult result = AnalysisResult.builder()
                .name(machineConfigName)
                .transitions(List.of(machineTransitions))
                .metadata(CodebaseMetadata.builder().callChains(List.of(chain)).build())
                .build();
        new TransitionLinkerEnricher().enrich(result, null, null);
        List<MatchedTransition> matched = result.getMetadata().getCallChains().get(0).getMatchedTransitions();
        assertThat(matched)
                .as("endpoint should resolve to exactly one SM transition")
                .hasSize(1);
        return matched.get(0);
    }

    static void assertLinkedEvent(MatchedTransition matched, String expectedEvent) {
        assertThat(matched.getEvent()).isEqualTo(expectedEvent);
    }

    static CallChain linkChain(
            CodebaseContext context,
            CallChain rawChain,
            String machineConfig,
            String eventTypeFqn,
            String stateTypeFqn,
            Transition... machineTransitions) {
        AnalysisResult result = AnalysisResult.builder()
                .name(machineConfig)
                .eventTypeFqn(eventTypeFqn)
                .stateTypeFqn(stateTypeFqn)
                .transitions(List.of(machineTransitions))
                .metadata(CodebaseMetadata.builder().callChains(List.of(rawChain)).build())
                .build();
        new TransitionLinkerEnricher().enrich(result, context, null);
        return result.getMetadata().getCallChains().get(0);
    }

    static CallChain resolveLinkAndAssertSingleMatch(
            CodebaseContext context,
            String entryClass,
            String entryMethod,
            String triggerClass,
            String triggerMethod,
            String machineConfig,
            String eventTypeFqn,
            String stateTypeFqn,
            String expectedLinkedEvent,
            EngineKind engineKind,
            Transition... machineTransitions) {
        CallChain raw = resolveChain(context, entryClass, entryMethod, triggerClass, triggerMethod, engineKind);
        CallChain linked = linkChain(context, raw, machineConfig, eventTypeFqn, stateTypeFqn, machineTransitions);
        assertPolyWithinTransitions(linked, machineTransitions.length);
        assertMatchedWithinTransitions(linked, machineTransitions.length);
        assertThat(linked.getMatchedTransitions())
                .as("expected exactly one matched transition")
                .hasSize(1);
        assertLinkedEvent(linked.getMatchedTransitions().get(0), expectedLinkedEvent);
        return linked;
    }

    static void assertPolyWithinTransitions(CallChain chain, int configuredTransitionCount) {
        List<String> poly = chain.getTriggerPoint().getPolymorphicEvents();
        assertThat(poly)
                .as("polymorphicEvents must be present after linking")
                .isNotNull();
        assertThat(poly.size())
                .as("polymorphicEvents must not exceed configured transition count")
                .isLessThanOrEqualTo(configuredTransitionCount);
        assertThat(MachineEnumCanonicalizer.hasOnlyConcretePolymorphicEvents(poly)).isTrue();
    }

    static void assertMatchedWithinTransitions(CallChain chain, int configuredTransitionCount) {
        assertThat(chain.getMatchedTransitions())
                .as("matchedTransitions should be populated after linking")
                .isNotNull();
        assertThat(chain.getMatchedTransitions().size())
                .as("matchedTransitions must not exceed configured transition count")
                .isLessThanOrEqualTo(configuredTransitionCount);
    }

    static void assertPolyCappedToTransitionEvents(
            CallChain chain,
            String eventTypeFqn,
            Transition... machineTransitions) {
        List<String> allowed = MachineEnumCanonicalizer.polymorphicEventsFromTransitions(
                List.of(machineTransitions), eventTypeFqn);
        assertThat(chain.getTriggerPoint().getPolymorphicEvents())
                .isNotNull()
                .allSatisfy(pe -> assertThat(allowed).contains(pe));
    }
}
