package click.kamil.springstatemachineexporter.analysis.enricher;

import click.kamil.springstatemachineexporter.analysis.model.CallChain;
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
import click.kamil.springstatemachineexporter.analysis.model.LinkResolution;
import click.kamil.springstatemachineexporter.analysis.model.MatchedTransition;
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
import org.junit.jupiter.api.Test;

import java.util.List;

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

class SemanticCallChainDedupTest {

    @Test
    void shouldPreferEntryAnchoredPathOverShorterOrphanPath() {
        EntryPoint entryPoint = EntryPoint.builder()
                .type(EntryPoint.Type.JMS)
                .name("JMS: order.queue")
                .className("com.example.JmsOrderListener")
                .methodName("onMessage")
                .build();
        TriggerPoint trigger = TriggerPoint.builder()
                .className("com.example.StateMachine")
                .methodName("sendEvent")
                .event("ORDER_EVENT")
                .build();
        List<MatchedTransition> matched = List.of(MatchedTransition.builder()
                .event("ORDER_EVENT")
                .sourceState("BUSY")
                .targetState("DONE")
                .build());

        CallChain anchoredLonger = CallChain.builder()
                .entryPoint(entryPoint)
                .triggerPoint(trigger)
                .linkResolution(LinkResolution.RESOLVED)
                .matchedTransitions(matched)
                .methodChain(List.of(
                        "com.example.JmsOrderListener.onMessage",
                        "com.example.OrderService.onMessage",
                        "com.example.StateMachine.sendEvent"))
                .build();
        CallChain orphanShorter = CallChain.builder()
                .entryPoint(entryPoint)
                .triggerPoint(trigger)
                .linkResolution(LinkResolution.RESOLVED)
                .matchedTransitions(matched)
                .methodChain(List.of("com.example.OrderService.onMessage"))
                .build();

        List<CallChain> deduped = SemanticCallChainDedup.deduplicate(List.of(orphanShorter, anchoredLonger));

        assertThat(deduped).hasSize(1);
        assertThat(deduped.get(0).getMethodChain())
                .containsExactly(
                        "com.example.JmsOrderListener.onMessage",
                        "com.example.OrderService.onMessage",
                        "com.example.StateMachine.sendEvent");
    }

    @Test
    void shouldCollapseChainsWithSameSemanticKeyButDifferentPaths() {
        EntryPoint entryPoint = EntryPoint.builder()
                .type(EntryPoint.Type.REST)
                .name("POST /api/order/pay")
                .className("com.example.Api")
                .methodName("pay")
                .build();
        TriggerPoint trigger = TriggerPoint.builder()
                .className("com.example.StateMachine")
                .methodName("sendEvent")
                .event("PAY")
                .build();
        List<MatchedTransition> matched = List.of(MatchedTransition.builder()
                .event("PAY")
                .sourceState("NEW")
                .targetState("PAID")
                .build());

        CallChain longPath = CallChain.builder()
                .entryPoint(entryPoint)
                .triggerPoint(trigger)
                .linkResolution(LinkResolution.RESOLVED)
                .matchedTransitions(matched)
                .methodChain(List.of("a", "b", "c", "d"))
                .build();
        CallChain shortPath = CallChain.builder()
                .entryPoint(entryPoint)
                .triggerPoint(trigger)
                .linkResolution(LinkResolution.RESOLVED)
                .matchedTransitions(matched)
                .methodChain(List.of("a", "b"))
                .build();

        List<CallChain> deduped = SemanticCallChainDedup.deduplicate(List.of(longPath, shortPath));

        assertThat(deduped).hasSize(1);
        assertThat(deduped.get(0).getMethodChain()).containsExactly("a", "b");
    }

    @Test
    void shouldKeepDistinctChainsWhenMatchedTransitionsDiffer() {
        EntryPoint entryPoint = EntryPoint.builder()
                .name("POST /api/order")
                .className("com.example.Api")
                .methodName("handle")
                .build();
        TriggerPoint trigger = TriggerPoint.builder()
                .className("com.example.StateMachine")
                .methodName("sendEvent")
                .event("PAY")
                .build();

        CallChain payChain = CallChain.builder()
                .entryPoint(entryPoint)
                .triggerPoint(trigger)
                .linkResolution(LinkResolution.RESOLVED)
                .matchedTransitions(List.of(MatchedTransition.builder()
                        .event("PAY")
                        .sourceState("NEW")
                        .targetState("PAID")
                        .build()))
                .methodChain(List.of("hop1"))
                .build();
        CallChain shipChain = CallChain.builder()
                .entryPoint(entryPoint)
                .triggerPoint(trigger)
                .linkResolution(LinkResolution.RESOLVED)
                .matchedTransitions(List.of(MatchedTransition.builder()
                        .event("SHIP")
                        .sourceState("PAID")
                        .targetState("SHIPPED")
                        .build()))
                .methodChain(List.of("hop1"))
                .build();

        List<CallChain> deduped = SemanticCallChainDedup.deduplicate(List.of(payChain, shipChain));

        assertThat(deduped).hasSize(2);
    }

    @Test
    void firstStrategyShouldKeepFirstSeenChain() {
        EntryPoint entryPoint = EntryPoint.builder()
                .name("JMS: orders")
                .className("com.example.Listener")
                .methodName("onMessage")
                .build();
        TriggerPoint trigger = TriggerPoint.builder()
                .className("com.example.StateMachine")
                .methodName("sendEvent")
                .event("PAY")
                .build();
        List<MatchedTransition> matched = List.of(MatchedTransition.builder()
                .event("PAY")
                .sourceState("NEW")
                .targetState("PAID")
                .build());

        CallChain first = CallChain.builder()
                .entryPoint(entryPoint)
                .triggerPoint(trigger)
                .linkResolution(LinkResolution.RESOLVED)
                .matchedTransitions(matched)
                .methodChain(List.of("long", "path"))
                .build();
        CallChain second = CallChain.builder()
                .entryPoint(entryPoint)
                .triggerPoint(trigger)
                .linkResolution(LinkResolution.RESOLVED)
                .matchedTransitions(matched)
                .methodChain(List.of("short"))
                .build();

        List<CallChain> deduped = SemanticCallChainDedup.deduplicate(
                List.of(first, second),
                SemanticCallChainDedup.KeepStrategy.FIRST);

        assertThat(deduped).hasSize(1);
        assertThat(deduped.get(0).getMethodChain()).containsExactly("long", "path");
    }

    @Test
    void mergeRefreshShouldReplaceExistingChainForSameEntryPoint() {
        EntryPoint entryPoint = EntryPoint.builder()
                .className("com.example.web.OrderController")
                .methodName("pay")
                .build();
        CallChain existing = CallChain.builder()
                .entryPoint(entryPoint)
                .triggerPoint(TriggerPoint.builder()
                        .className("com.example.web.OrderStateMachine")
                        .methodName("sendEvent")
                        .event("event")
                        .build())
                .methodChain(null)
                .build();
        CallChain refreshed = CallChain.builder()
                .entryPoint(entryPoint)
                .triggerPoint(TriggerPoint.builder()
                        .className("com.example.web.OrderController")
                        .methodName("pay")
                        .event("PAY")
                        .build())
                .methodChain(List.of("com.example.web.OrderController.pay"))
                .build();

        List<CallChain> merged = SemanticCallChainDedup.mergeRefresh(
                List.of(existing),
                List.of(refreshed),
                SemanticCallChainDedup.KeepStrategy.SHORTEST);

        assertThat(merged).hasSize(1);
        assertThat(merged.get(0).getMethodChain()).containsExactly("com.example.web.OrderController.pay");
        assertThat(merged.get(0).getTriggerPoint().getEvent()).isEqualTo("PAY");
    }

    @Test
    void mergeRefreshShouldReplaceSameEntryAndTriggerEvent() {
        EntryPoint entryPoint = EntryPoint.builder()
                .className("com.example.web.OrderController")
                .methodName("pay")
                .build();
        CallChain existing = CallChain.builder()
                .entryPoint(entryPoint)
                .triggerPoint(TriggerPoint.builder()
                        .className("com.example.web.OrderStateMachine")
                        .methodName("sendEvent")
                        .event("PAY")
                        .build())
                .methodChain(null)
                .build();
        CallChain refreshed = CallChain.builder()
                .entryPoint(entryPoint)
                .triggerPoint(TriggerPoint.builder()
                        .className("com.example.web.OrderController")
                        .methodName("pay")
                        .event("PAY")
                        .build())
                .methodChain(List.of("com.example.web.OrderController.pay"))
                .build();

        List<CallChain> merged = SemanticCallChainDedup.mergeRefresh(
                List.of(existing),
                List.of(refreshed),
                SemanticCallChainDedup.KeepStrategy.SHORTEST);

        assertThat(merged).hasSize(1);
        assertThat(merged.get(0).getMethodChain()).containsExactly("com.example.web.OrderController.pay");
        assertThat(merged.get(0).getTriggerPoint().getMethodName()).isEqualTo("pay");
    }

    @Test
    void mergeRefreshShouldKeepMultiEventFanOutFromSameEntryPoint() {
        EntryPoint entryPoint = EntryPoint.builder()
                .name("POST /api/order")
                .className("com.example.Api")
                .methodName("handle")
                .build();
        CallChain existingPay = CallChain.builder()
                .entryPoint(entryPoint)
                .triggerPoint(TriggerPoint.builder()
                        .className("com.example.StateMachine")
                        .methodName("sendEvent")
                        .event("PAY")
                        .build())
                .methodChain(List.of("com.example.Api.handle", "pay"))
                .build();
        CallChain refreshedShip = CallChain.builder()
                .entryPoint(entryPoint)
                .triggerPoint(TriggerPoint.builder()
                        .className("com.example.StateMachine")
                        .methodName("sendEvent")
                        .event("SHIP")
                        .build())
                .methodChain(List.of("com.example.Api.handle", "ship"))
                .build();
        CallChain refreshedPay = CallChain.builder()
                .entryPoint(entryPoint)
                .triggerPoint(TriggerPoint.builder()
                        .className("com.example.StateMachine")
                        .methodName("sendEvent")
                        .event("PAY")
                        .build())
                .methodChain(List.of("com.example.Api.handle"))
                .build();

        List<CallChain> merged = SemanticCallChainDedup.mergeRefresh(
                List.of(existingPay),
                List.of(refreshedShip, refreshedPay),
                SemanticCallChainDedup.KeepStrategy.SHORTEST);

        assertThat(merged).hasSize(2);
        assertThat(merged)
                .anySatisfy(chain -> {
                    assertThat(chain.getTriggerPoint().getEvent()).isEqualTo("PAY");
                    assertThat(chain.getMethodChain()).containsExactly("com.example.Api.handle");
                });
        assertThat(merged)
                .anySatisfy(chain -> assertThat(chain.getTriggerPoint().getEvent()).isEqualTo("SHIP"));
    }

    @Test
    void mergeShouldCollapseJsonStubChainWithRefreshedEntryPoint() {
        TriggerPoint trigger = TriggerPoint.builder()
                .className("com.example.web.OrderController")
                .methodName("pay")
                .event("PAY")
                .sourceState("NEW")
                .build();
        List<MatchedTransition> matched = List.of(MatchedTransition.builder()
                .event("PAY")
                .sourceState("NEW")
                .targetState("PAID")
                .build());

        CallChain jsonStub = CallChain.builder()
                .triggerPoint(trigger)
                .linkResolution(LinkResolution.RESOLVED)
                .matchedTransitions(matched)
                .build();
        CallChain refreshed = CallChain.builder()
                .entryPoint(EntryPoint.builder()
                        .className("com.example.web.OrderController")
                        .methodName("pay")
                        .build())
                .triggerPoint(trigger)
                .linkResolution(LinkResolution.RESOLVED)
                .matchedTransitions(matched)
                .methodChain(List.of("com.example.web.OrderController.pay"))
                .build();

        List<CallChain> merged = SemanticCallChainDedup.merge(
                List.of(jsonStub),
                List.of(refreshed),
                SemanticCallChainDedup.KeepStrategy.SHORTEST);

        assertThat(merged).hasSize(1);
        assertThat(merged.get(0).getEntryPoint()).isNotNull();
        assertThat(merged.get(0).getMethodChain()).containsExactly("com.example.web.OrderController.pay");
    }

    @Test
    void mergeShouldSemanticDedupAcrossExistingAndRefreshedLists() {
        EntryPoint entryPoint = EntryPoint.builder()
                .name("POST /api/order/pay")
                .className("com.example.Api")
                .methodName("pay")
                .build();
        TriggerPoint trigger = TriggerPoint.builder()
                .className("com.example.StateMachine")
                .methodName("sendEvent")
                .event("PAY")
                .build();
        List<MatchedTransition> matched = List.of(MatchedTransition.builder()
                .event("PAY")
                .sourceState("NEW")
                .targetState("PAID")
                .build());

        CallChain existing = CallChain.builder()
                .entryPoint(entryPoint)
                .triggerPoint(trigger)
                .linkResolution(LinkResolution.RESOLVED)
                .matchedTransitions(matched)
                .methodChain(List.of("a", "b", "c"))
                .build();
        CallChain refreshed = CallChain.builder()
                .entryPoint(entryPoint)
                .triggerPoint(trigger)
                .linkResolution(LinkResolution.RESOLVED)
                .matchedTransitions(matched)
                .methodChain(List.of("a"))
                .build();

        List<CallChain> merged = SemanticCallChainDedup.merge(
                List.of(existing),
                List.of(refreshed),
                SemanticCallChainDedup.KeepStrategy.SHORTEST);

        assertThat(merged).hasSize(1);
        assertThat(merged.get(0).getMethodChain()).containsExactly("a");
    }
}
