package click.kamil.springstatemachineexporter.analysis.service;

import click.kamil.springstatemachineexporter.analysis.model.CallChain;
import click.kamil.springstatemachineexporter.analysis.model.CallEdge;
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 java.util.Map;

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

/**
 * Template-method bases call abstract hooks as plain {@code doPMessage(m)} (no {@code this.}).
 * That null-receiver form must still expand to concrete subclass overrides in the call graph —
 * otherwise JMS listeners that share the base never reach subclass triggers.
 */
class ImplicitThisTemplateMethodCallGraphTest {

    @Test
    void plainHookCallWithoutThisExpandsToSubclassOverrides(@TempDir Path tempDir) throws Exception {
        writeTemplateMethodFixture(tempDir);

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

        List<CallEdge> fromProcess = graph.get("com.example.AbstractListener.processMessage");
        assertThat(fromProcess)
                .extracting(CallEdge::getTargetMethod)
                .as("doPMessage(m) must expand like this.doPMessage(m) for abstract hooks")
                .contains(
                        "com.example.OrderListener.doPMessage",
                        "com.example.DocumentListener.doPMessage")
                .doesNotContain("com.example.AbstractListener.doPMessage");

        List<CallEdge> fromOnMessage = graph.get("com.example.OrderListener.onMessage");
        assertThat(fromOnMessage)
                .extracting(CallEdge::getTargetMethod)
                .as("concrete template method processMessage must not invent subclass keys")
                .containsExactly("com.example.AbstractListener.processMessage");
    }

    @Test
    void jmsEntryReachesSubclassTriggerThroughPlainTemplateHook(@TempDir Path tempDir) throws Exception {
        writeTemplateMethodFixture(tempDir);

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

        List<CallChain> orderChains = engine.findChains(
                List.of(EntryPoint.builder()
                        .type(EntryPoint.Type.JMS)
                        .name("JMS: orders")
                        .className("com.example.OrderListener")
                        .methodName("onMessage")
                        .build()),
                List.of(TriggerPoint.builder()
                        .className("com.example.StateMachine")
                        .methodName("sendEvent")
                        .event("event")
                        .build()));

        assertThat(orderChains)
                .as("OrderListener.onMessage -> inherited processMessage -> OrderListener.doPMessage -> sendEvent")
                .isNotEmpty();
        assertThat(orderChains)
                .as("context-sensitive search must never walk sibling DocumentListener frames")
                .allSatisfy(chain -> assertThat(chain.getMethodChain())
                        .noneMatch(hop -> hop.contains("DocumentListener")));
        assertThat(orderChains)
                .allSatisfy(chain -> assertThat(chain.getMethodChain())
                        .anyMatch(hop -> hop.contains("OrderListener.doPMessage")));
        assertThat(orderChains.stream()
                .flatMap(chain -> {
                    List<String> events = chain.getTriggerPoint().getPolymorphicEvents();
                    return events == null ? java.util.stream.Stream.<String>empty() : events.stream();
                }))
                .anyMatch(e -> e.contains("PAY") || e.endsWith(".PAY"));

        List<CallChain> documentChains = engine.findChains(
                List.of(EntryPoint.builder()
                        .type(EntryPoint.Type.JMS)
                        .name("JMS: documents")
                        .className("com.example.DocumentListener")
                        .methodName("onMessage")
                        .build()),
                List.of(TriggerPoint.builder()
                        .className("com.example.StateMachine")
                        .methodName("sendEvent")
                        .event("event")
                        .build()));

        assertThat(documentChains).isNotEmpty();
        assertThat(documentChains)
                .allSatisfy(chain -> assertThat(chain.getMethodChain())
                        .noneMatch(hop -> hop.contains("OrderListener")));
        assertThat(documentChains)
                .allSatisfy(chain -> assertThat(chain.getMethodChain())
                        .anyMatch(hop -> hop.contains("DocumentListener.doPMessage")));
        assertThat(documentChains.stream()
                .flatMap(chain -> {
                    List<String> events = chain.getTriggerPoint().getPolymorphicEvents();
                    return events == null ? java.util.stream.Stream.<String>empty() : events.stream();
                }))
                .anyMatch(e -> e.contains("SUBMIT") || e.endsWith(".SUBMIT"));
    }

    @Test
    void explicitThisHookCallStillExpandsTheSameWay(@TempDir Path tempDir) throws Exception {
        Files.writeString(tempDir.resolve("Listener.java"), """
                package com.example;
                abstract class AbstractListener {
                    void processMessage(String m) {
                        this.doPMessage(m);
                    }
                    abstract void doPMessage(String m);
                }
                class OrderListener extends AbstractListener {
                    StateMachine sm = new StateMachine();
                    void onMessage(String m) { processMessage(m); }
                    @Override
                    void doPMessage(String m) { sm.sendEvent(OrderEvent.PAY); }
                }
                class StateMachine { void sendEvent(OrderEvent event) {} }
                enum OrderEvent { PAY }
                """);

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

        assertThat(graph.get("com.example.AbstractListener.processMessage"))
                .extracting(CallEdge::getTargetMethod)
                .containsExactly("com.example.OrderListener.doPMessage");
    }

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

    private static void writeTemplateMethodFixture(Path tempDir) throws Exception {
        Files.writeString(tempDir.resolve("Listeners.java"), """
                package com.example;

                abstract class AbstractListener {
                    /** Template method: hook invoked without an explicit this. receiver. */
                    void processMessage(String m) {
                        doPMessage(m);
                    }
                    abstract void doPMessage(String m);
                }

                class OrderListener extends AbstractListener {
                    private final StateMachine sm = new StateMachine();
                    void onMessage(String m) {
                        processMessage(m);
                    }
                    @Override
                    void doPMessage(String m) {
                        sm.sendEvent(OrderEvent.PAY);
                    }
                }

                class DocumentListener extends AbstractListener {
                    private final StateMachine sm = new StateMachine();
                    void onMessage(String m) {
                        processMessage(m);
                    }
                    @Override
                    void doPMessage(String m) {
                        sm.sendEvent(DocumentEvent.SUBMIT);
                    }
                }

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