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.nio.file.Files;
import java.nio.file.Path;
import java.util.List;

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

/**
 * When {@code @JmsListener} is declared on an abstract base / interface, findChains must expand
 * to concrete implementations with per-impl {@code concreteSelf} so template hooks reach
 * {@code sendEvent} without sibling cross-wiring.
 */
class JmsAbstractEntryExpansionTest {

    @Test
    void abstractJmsListenerExpandsToConcreteSubclassChains(@TempDir Path tempDir) throws Exception {
        Files.writeString(tempDir.resolve("Listeners.java"), """
                package com.example;

                @interface JmsListener {
                    String destination() default "";
                }

                abstract class AbstractAA {
                    @JmsListener(destination = "orders")
                    void onMessage(String m) {
                        processMessage(m);
                    }
                    void processMessage(String m) {
                        doPMessage(m);
                    }
                    abstract void doPMessage(String m);
                }

                class OrderAA extends AbstractAA {
                    private final StateMachine sm = new StateMachine();
                    @Override
                    void doPMessage(String m) {
                        sm.sendEvent(OrderEvent.PAY);
                    }
                }

                class DocumentAA extends AbstractAA {
                    private final StateMachine sm = new StateMachine();
                    @Override
                    void doPMessage(String m) {
                        sm.sendEvent(DocumentEvent.SUBMIT);
                    }
                }

                class StateMachine {
                    void sendEvent(Object event) {}
                }
                enum OrderEvent { PAY }
                enum DocumentEvent { SUBMIT }
                """);

        CodebaseContext context = scan(tempDir);
        List<EntryPoint> entryPoints = detectJms(context);
        assertThat(entryPoints)
                .anySatisfy(ep -> assertThat(ep.getClassName()).isEqualTo("com.example.AbstractAA"));

        HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
        List<CallChain> chains = engine.findChains(
                entryPoints.stream().filter(ep -> "onMessage".equals(ep.getMethodName())).toList(),
                List.of(TriggerPoint.builder()
                        .className("com.example.StateMachine")
                        .methodName("sendEvent")
                        .event("event")
                        .build()));

        assertThat(chains)
                .as("abstract JMS EP must expand to concrete subclass paths")
                .isNotEmpty();
        assertThat(chains)
                .anyMatch(chain -> chain.getMethodChain().stream()
                        .anyMatch(hop -> hop.contains("OrderAA.doPMessage")));
        assertThat(chains)
                .anyMatch(chain -> chain.getMethodChain().stream()
                        .anyMatch(hop -> hop.contains("DocumentAA.doPMessage")));
        assertThat(chains)
                .filteredOn(chain -> chain.getMethodChain().stream()
                        .anyMatch(hop -> hop.contains("OrderAA.doPMessage")))
                .allSatisfy(chain -> assertThat(chain.getMethodChain())
                        .noneMatch(hop -> hop.contains("DocumentAA")));
        assertThat(chains)
                .filteredOn(chain -> chain.getMethodChain().stream()
                        .anyMatch(hop -> hop.contains("DocumentAA.doPMessage")))
                .allSatisfy(chain -> assertThat(chain.getMethodChain())
                        .noneMatch(hop -> hop.contains("OrderAA")));
    }

    @Test
    void interfaceJmsListenerWithMultipleImplsYieldsIsolatedChains(@TempDir Path tempDir) throws Exception {
        Files.writeString(tempDir.resolve("Iface.java"), """
                package com.example;

                @interface JmsListener {
                    String destination() default "";
                }

                interface OrderCommands {
                    @JmsListener(destination = "orders")
                    void onMessage(String m);
                }

                class PayListener implements OrderCommands {
                    private final StateMachine sm = new StateMachine();
                    @Override
                    public void onMessage(String m) {
                        sm.sendEvent(OrderEvent.PAY);
                    }
                }

                class ShipListener implements OrderCommands {
                    private final StateMachine sm = new StateMachine();
                    @Override
                    public void onMessage(String m) {
                        sm.sendEvent(OrderEvent.SHIP);
                    }
                }

                class StateMachine {
                    void sendEvent(Object event) {}
                }
                enum OrderEvent { PAY, SHIP }
                """);

        CodebaseContext context = scan(tempDir);
        List<EntryPoint> entryPoints = detectJms(context);
        assertThat(entryPoints)
                .anySatisfy(ep -> assertThat(ep.getClassName()).isEqualTo("com.example.OrderCommands"));

        HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
        List<CallChain> chains = engine.findChains(
                entryPoints,
                List.of(TriggerPoint.builder()
                        .className("com.example.StateMachine")
                        .methodName("sendEvent")
                        .event("event")
                        .build()));

        assertThat(chains).isNotEmpty();
        assertThat(chains)
                .anyMatch(chain -> chain.getMethodChain().stream()
                        .anyMatch(hop -> hop.contains("PayListener")));
        assertThat(chains)
                .anyMatch(chain -> chain.getMethodChain().stream()
                        .anyMatch(hop -> hop.contains("ShipListener")));
        assertThat(chains)
                .filteredOn(chain -> chain.getMethodChain().stream()
                        .anyMatch(hop -> hop.contains("PayListener")))
                .allSatisfy(chain -> assertThat(chain.getMethodChain())
                        .noneMatch(hop -> hop.contains("ShipListener")));
    }

    private static List<EntryPoint> detectJms(CodebaseContext context) {
        MessagingDetector detector = new MessagingDetector(context);
        return context.getCompilationUnits().stream()
                .flatMap(cu -> detector.detect(cu).stream())
                .filter(ep -> ep.getType() == EntryPoint.Type.JMS)
                .toList();
    }

    private static CodebaseContext scan(Path tempDir) throws Exception {
        CodebaseContext context = new CodebaseContext();
        context.setResolveBindings(true);
        context.scan(tempDir);
        return context;
    }
}
