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.LinkResolution;
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
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 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.Map;

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

/**
 * JMS payload params must enter the same binding-expansion path as REST PathVariables.
 */
class JmsPayloadBindingTest {

    @Test
    void shouldExpandJmsMessagePayloadFromStringSwitchMapper(@TempDir Path tempDir) throws Exception {
        writeJmsSwitchProject(tempDir);

        CodebaseContext context = scan(tempDir);
        HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
        Map<String, List<click.kamil.springstatemachineexporter.analysis.model.CallEdge>> graph =
                engine.buildCallGraph();

        EntryPoint entryPoint = jmsEntryPoint();

        List<Map<String, String>> variants =
                EntryPointBindingExpander.expandPathVariableBindings(entryPoint, context, graph);

        assertThat(variants).extracting(map -> map.get("message"))
                .containsExactlyInAnyOrder("PAY", "SHIP");
    }

    @Test
    void shouldProduceSeparateChainsForJmsPayloadSwitchCases(@TempDir Path tempDir) throws Exception {
        writeJmsSwitchProject(tempDir);

        CodebaseContext context = scan(tempDir);
        HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);

        List<CallChain> chains = engine.findChains(
                List.of(jmsEntryPoint()),
                List.of(TriggerPoint.builder()
                        .className("com.example.OrderService")
                        .methodName("fire")
                        .event("event")
                        .build()));

        assertThat(chains).hasSize(2);
        assertThat(chains.stream().map(chain -> chain.getEntryPoint().getName()))
                .containsExactlyInAnyOrder(
                        "JMS: order.commands?message=PAY",
                        "JMS: order.commands?message=SHIP");
    }

    @Test
    void shouldLinkExpandedJmsPayloadChainsToMatchingTransitions(@TempDir Path tempDir) throws Exception {
        writeJmsSwitchProject(tempDir);

        CodebaseContext context = scan(tempDir);
        HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);

        List<CallChain> chains = engine.findChains(
                List.of(jmsEntryPoint()),
                List.of(TriggerPoint.builder()
                        .className("com.example.OrderService")
                        .methodName("fire")
                        .event("event")
                        .build()));

        AnalysisResult result = AnalysisResult.builder()
                .name("OrderStateMachineConfig")
                .eventTypeFqn("com.example.OrderEvent")
                .transitions(List.of(
                        transition("PAY", "NEW", "PAID"),
                        transition("SHIP", "PAID", "SHIPPED")))
                .metadata(CodebaseMetadata.builder().callChains(chains).build())
                .build();

        new TransitionLinkerEnricher().enrich(result, context, null);

        List<CallChain> linked = result.getMetadata().getCallChains();
        assertThat(linked).hasSize(2);
        assertThat(linked).allSatisfy(chain -> {
            assertThat(chain.getLinkResolution()).isEqualTo(LinkResolution.RESOLVED);
            assertThat(chain.getMatchedTransitions()).hasSize(1);
        });
        assertThat(linked.stream()
                .flatMap(chain -> chain.getMatchedTransitions().stream())
                .map(mt -> mt.getEvent())
                .toList())
                .containsExactlyInAnyOrder(
                        "com.example.OrderEvent.PAY",
                        "com.example.OrderEvent.SHIP");
    }

    @Test
    void shouldDetectJmsListenerEntryPoint(@TempDir Path tempDir) throws Exception {
        writeJmsSwitchProject(tempDir);
        CodebaseContext context = scan(tempDir);

        MessagingDetector detector = new MessagingDetector(context);
        List<EntryPoint> entryPoints = context.getCompilationUnits().stream()
                .flatMap(cu -> detector.detect(cu).stream())
                .toList();

        assertThat(entryPoints).anySatisfy(ep -> {
            assertThat(ep.getType()).isEqualTo(EntryPoint.Type.JMS);
            assertThat(ep.getName()).isEqualTo("JMS: order.commands");
            assertThat(ep.getMethodName()).isEqualTo("onMessage");
        });
    }

    private static EntryPoint jmsEntryPoint() {
        return EntryPoint.builder()
                .type(EntryPoint.Type.JMS)
                .name("JMS: order.commands")
                .className("com.example.OrderJmsListener")
                .methodName("onMessage")
                .metadata(Map.of("destination", "order.commands", "protocol", "JMS"))
                .parameters(List.of(EntryPoint.Parameter.builder()
                        .name("message")
                        .type("String")
                        .annotations(List.of())
                        .build()))
                .build();
    }

    private static void writeJmsSwitchProject(Path tempDir) throws Exception {
        Path javaRoot = tempDir.resolve("src/main/java/com/example");
        Files.createDirectories(javaRoot);
        Files.writeString(tempDir.resolve("build.gradle"), "plugins { id 'java' }");
        Files.writeString(javaRoot.resolve("OrderEvent.java"), """
                package com.example;
                public enum OrderEvent { PAY, SHIP, LOG }
                """);
        Files.writeString(javaRoot.resolve("OrderService.java"), """
                package com.example;
                public class OrderService {
                    void fire(OrderEvent event) {}
                    void dispatch(String message) {
                        OrderEvent event = switch (message) {
                            case "PAY" -> OrderEvent.PAY;
                            case "SHIP" -> OrderEvent.SHIP;
                            default -> throw new IllegalArgumentException(message);
                        };
                        fire(event);
                    }
                }
                """);
        Files.writeString(javaRoot.resolve("OrderJmsListener.java"), """
                package com.example;
                import org.springframework.jms.annotation.JmsListener;
                public class OrderJmsListener {
                    private final OrderService orderService = new OrderService();
                    @JmsListener(destination = "order.commands")
                    public void onMessage(String message) {
                        orderService.dispatch(message);
                    }
                }
                """);
        Files.writeString(javaRoot.resolve("JmsListener.java"), """
                package org.springframework.jms.annotation;
                import java.lang.annotation.Retention;
                import java.lang.annotation.RetentionPolicy;
                @Retention(RetentionPolicy.RUNTIME)
                public @interface JmsListener {
                    String destination() default "";
                }
                """);
    }

    private static CodebaseContext scan(Path tempDir) throws Exception {
        CodebaseContext context = new CodebaseContext();
        context.setResolveBindings(true);
        context.setSourcepath(List.of(tempDir.resolve("src/main/java").toString()));
        context.scan(tempDir);
        return context;
    }

    private static Transition transition(String event, String source, String target) {
        Transition transition = new Transition();
        transition.setEvent(Event.of(event, "com.example.OrderEvent." + event));
        transition.setSourceStates(List.of(State.of(source, "com.example.OrderState." + source)));
        transition.setTargetStates(List.of(State.of(target, "com.example.OrderState." + target)));
        return transition;
    }
}
