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;

/**
 * JMS listeners often call a field typed as an abstract collaborator ({@code ops.process()}).
 * CHA expands to each impl target and must stamp {@code receiverTypeFqn} so pathfinding can
 * walk that impl's template body without sibling cross-wiring — and still produce edges.
 */
class JmsCollaboratorAbstractFieldCallGraphTest {

    @Test
    void jmsListenerThroughAbstractOpsFieldReachesSendEvent(@TempDir Path tempDir) throws Exception {
        writeFixture(tempDir);

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

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

        List<CallEdge> fromOnMessage = graph.get("com.example.OrderJmsListener.onMessage");
        assertThat(fromOnMessage)
                .extracting(CallEdge::getTargetMethod)
                .as("ops.process() must CHA-expand to concrete collaborator impls")
                .contains(
                        "com.example.PayOps.process",
                        "com.example.ShipOps.process");
        assertThat(fromOnMessage)
                .filteredOn(edge -> edge.getTargetMethod().contains("PayOps"))
                .extracting(CallEdge::getReceiverTypeFqn)
                .containsOnly("com.example.PayOps");

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

        assertThat(chains)
                .as("JMS → abstract ops field → fire → sendEvent must produce chains")
                .isNotEmpty();

        assertThat(chains)
                .anyMatch(chain -> chain.getMethodChain().stream()
                        .anyMatch(hop -> hop.contains("PayOps")));
        assertThat(chains)
                .filteredOn(chain -> chain.getMethodChain().stream()
                        .anyMatch(hop -> hop.contains("PayOps")))
                .allSatisfy(chain -> assertThat(chain.getMethodChain())
                        .noneMatch(hop -> hop.contains("ShipOps")));
    }

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

                class OrderJmsListener {
                    private AbstractOps ops;
                    void onMessage(String m) {
                        ops.process(m);
                    }
                }

                abstract class AbstractOps {
                    void process(String e) {
                        fire(e);
                    }
                    abstract void fire(String e);
                }

                class PayOps extends AbstractOps {
                    private final StateMachine sm = new StateMachine();
                    @Override
                    void fire(String e) {
                        sm.sendEvent(OrderEvent.PAY);
                    }
                }

                class ShipOps extends AbstractOps {
                    private final StateMachine sm = new StateMachine();
                    @Override
                    void fire(String e) {
                        sm.sendEvent(OrderEvent.SHIP);
                    }
                }

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