package click.kamil.springstatemachineexporter.analysis.enricher;

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.model.Event;
import click.kamil.springstatemachineexporter.model.State;
import click.kamil.springstatemachineexporter.model.Transition;
import org.junit.jupiter.api.Test;

import java.util.List;

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

class TransitionLinkerEnricherTest {

    private final TransitionLinkerEnricher enricher = new TransitionLinkerEnricher();

    @Test
    void shouldLinkWhenPolymorphicEventsContainConcreteEnumConstant() {
        Transition payT = new Transition();
        payT.setSourceStates(List.of(State.of("NEW", "OrderState.NEW")));
        payT.setTargetStates(List.of(State.of("PAID", "OrderState.PAID")));
        payT.setEvent(Event.of("OrderEvent.PAY", "com.example.OrderEvent.PAY"));

        CallChain chain = CallChain.builder()
                .triggerPoint(TriggerPoint.builder()
                        .event("e")
                        .polymorphicEvents(List.of("OrderEvent.PAY"))
                        .build())
                .build();

        AnalysisResult result = AnalysisResult.builder()
                .name("OrderStateMachineConfig")
                .transitions(List.of(payT))
                .metadata(CodebaseMetadata.builder().callChains(List.of(chain)).build())
                .build();

        enricher.enrich(result, null, null);

        assertThat(result.getMetadata().getCallChains().get(0).getMatchedTransitions()).hasSize(1);
    }

    @Test
    void shouldExportResolvedLinkResolutionForDedicatedLiteralEndpoint() {
        Transition payT = new Transition();
        payT.setSourceStates(List.of(State.of("NEW", "OrderState.NEW")));
        payT.setTargetStates(List.of(State.of("PAID", "OrderState.PAID")));
        payT.setEvent(Event.of("OrderEvent.PAY", "com.example.OrderEvent.PAY"));

        CallChain chain = CallChain.builder()
                .entryPoint(EntryPoint.builder()
                        .type(EntryPoint.Type.REST)
                        .name("POST /api/order/pay")
                        .className("com.example.Api")
                        .methodName("pay")
                        .build())
                .triggerPoint(TriggerPoint.builder()
                        .event("OrderEvent.PAY")
                        .polymorphicEvents(List.of("OrderEvent.PAY"))
                        .external(false)
                        .build())
                .build();

        AnalysisResult result = AnalysisResult.builder()
                .name("OrderStateMachineConfig")
                .transitions(List.of(payT))
                .metadata(CodebaseMetadata.builder().callChains(List.of(chain)).build())
                .build();

        enricher.enrich(result, null, null);

        CallChain updated = result.getMetadata().getCallChains().get(0);
        assertThat(updated.getLinkResolution()).isEqualTo(LinkResolution.RESOLVED);
        assertThat(updated.getMatchedTransitions()).hasSize(1);
    }

    @Test
    void shouldNotLinkAllTransitionsWhenNonEventEnumConstantIsBound() {
        Transition pay = new Transition();
        pay.setSourceStates(List.of(State.of("NEW", "com.example.OrderState.NEW")));
        pay.setTargetStates(List.of(State.of("PAID", "com.example.OrderState.PAID")));
        pay.setEvent(Event.of("PAY", "com.example.OrderEvent.PAY"));

        Transition ship = new Transition();
        ship.setSourceStates(List.of(State.of("PAID", "com.example.OrderState.PAID")));
        ship.setTargetStates(List.of(State.of("SHIPPED", "com.example.OrderState.SHIPPED")));
        ship.setEvent(Event.of("SHIP", "com.example.OrderEvent.SHIP"));

        // adjust/LOG-style: dataflow proves a non-transition / non-event constant.
        CallChain chain = CallChain.builder()
                .entryPoint(EntryPoint.builder()
                        .type(EntryPoint.Type.REST)
                        .name("POST /api/order/adjust")
                        .className("com.example.OrderController")
                        .methodName("adjust")
                        .build())
                .pathBindings(java.util.Map.of("event", "com.example.OrderEvent.LOG"))
                .triggerPoint(TriggerPoint.builder()
                        .event("com.example.OrderEvent.LOG")
                        .polymorphicEvents(List.of("com.example.OrderEvent.LOG"))
                        .build())
                .build();

        AnalysisResult result = AnalysisResult.builder()
                .name("OrderStateMachineConfig")
                .eventTypeFqn("com.example.OrderEvent")
                .transitions(List.of(pay, ship))
                .metadata(CodebaseMetadata.builder().callChains(List.of(chain)).build())
                .build();

        enricher.enrich(result, null, null);

        CallChain updated = result.getMetadata().getCallChains().get(0);
        assertThat(updated.getMatchedTransitions()).isNullOrEmpty();
        assertThat(updated.getLinkResolution()).isIn(LinkResolution.NO_MATCH, LinkResolution.AMBIGUOUS_WIDEN);
    }

    @Test
    void shouldFailClosedWhenIsEventFilterLeavesMultipleTransitionEventsWithoutEndpointEvidence() {
        Transition pay = new Transition();
        pay.setSourceStates(List.of(State.of("NEW", "com.example.OrderState.NEW")));
        pay.setTargetStates(List.of(State.of("PAID", "com.example.OrderState.PAID")));
        pay.setEvent(Event.of("PAY", "com.example.OrderEvent.PAY"));

        Transition ship = new Transition();
        ship.setSourceStates(List.of(State.of("PAID", "com.example.OrderState.PAID")));
        ship.setTargetStates(List.of(State.of("SHIPPED", "com.example.OrderState.SHIPPED")));
        ship.setEvent(Event.of("SHIP", "com.example.OrderEvent.SHIP"));

        CallChain chain = CallChain.builder()
                .entryPoint(EntryPoint.builder()
                        .type(EntryPoint.Type.REST)
                        .name("POST /api/order/adjust")
                        .className("com.example.OrderController")
                        .methodName("adjust")
                        .build())
                .triggerPoint(TriggerPoint.builder()
                        .ambiguous(false)
                        .event("OrderEvent.valueOf(eventStr)")
                        .polymorphicEvents(List.of(
                                "com.example.OrderEvent.PAY",
                                "com.example.OrderEvent.SHIP"))
                        // After dataflow predicate filter: only transition events remain, still multi.
                        .build())
                .build();

        AnalysisResult result = AnalysisResult.builder()
                .name("OrderStateMachineConfig")
                .eventTypeFqn("com.example.OrderEvent")
                .transitions(List.of(pay, ship))
                .metadata(CodebaseMetadata.builder().callChains(List.of(chain)).build())
                .build();

        enricher.enrich(result, null, null);

        CallChain updated = result.getMetadata().getCallChains().get(0);
        assertThat(updated.getMatchedTransitions()).isNullOrEmpty();
        assertThat(updated.getLinkResolution()).isEqualTo(LinkResolution.AMBIGUOUS_WIDEN);
    }

    @Test
    void shouldExportUnresolvedExternalForGenericPathVariableEndpoint() {
        CallChain chain = CallChain.builder()
                .entryPoint(EntryPoint.builder()
                        .type(EntryPoint.Type.REST)
                        .name("POST /api/machine/{machineType}/transition/{event}")
                        .className("com.example.Api")
                        .methodName("transition")
                        .build())
                .triggerPoint(TriggerPoint.builder()
                        .event("eventString.toUpperCase()")
                        .external(true)
                        .ambiguous(true)
                        .build())
                .build();

        Transition payT = new Transition();
        payT.setSourceStates(List.of(State.of("NEW", "OrderState.NEW")));
        payT.setTargetStates(List.of(State.of("PAID", "OrderState.PAID")));
        payT.setEvent(Event.of("OrderEvent.PAY", "com.example.OrderEvent.PAY"));

        AnalysisResult result = AnalysisResult.builder()
                .name("OrderStateMachineConfig")
                .transitions(List.of(payT))
                .metadata(CodebaseMetadata.builder().callChains(List.of(chain)).build())
                .build();

        enricher.enrich(result, null, null);

        CallChain updated = result.getMetadata().getCallChains().get(0);
        assertThat(updated.getLinkResolution()).isEqualTo(LinkResolution.UNRESOLVED_EXTERNAL);
        assertThat(updated.getMatchedTransitions()).isNullOrEmpty();
    }

    @Test
    void shouldResolveDedicatedEndpointEvenWhenMisTaggedExternal() {
        Transition payT = new Transition();
        payT.setSourceStates(List.of(State.of("NEW", "com.example.OrderState.NEW")));
        payT.setTargetStates(List.of(State.of("PAID", "com.example.OrderState.PAID")));
        payT.setEvent(Event.of("PAY", "com.example.OrderEvent.PAY"));

        CallChain chain = CallChain.builder()
                .entryPoint(EntryPoint.builder()
                        .type(EntryPoint.Type.REST)
                        .name("POST /api/machine/order/pay")
                        .className("com.example.Api")
                        .methodName("payOrder")
                        .parameters(List.of(EntryPoint.Parameter.builder()
                                .name("userId")
                                .type("String")
                                .annotations(List.of("RequestParam"))
                                .build()))
                        .build())
                .triggerPoint(TriggerPoint.builder()
                        .event("com.example.OrderEvent.PAY")
                        .polymorphicEvents(List.of("com.example.OrderEvent.PAY"))
                        .external(true)
                        .build())
                .build();

        AnalysisResult result = AnalysisResult.builder()
                .name("OrderStateMachineConfig")
                .eventTypeFqn("com.example.OrderEvent")
                .transitions(List.of(payT))
                .metadata(CodebaseMetadata.builder().callChains(List.of(chain)).build())
                .build();

        enricher.enrich(result, null, null);

        CallChain updated = result.getMetadata().getCallChains().get(0);
        assertThat(updated.getLinkResolution()).isEqualTo(LinkResolution.RESOLVED);
        assertThat(updated.getMatchedTransitions()).hasSize(1);
        assertThat(updated.getTriggerPoint().isExternal()).isFalse();
    }

    @Test
    void shouldLinkDedicatedEventEndpointEvenWithResourceIdPlaceholder() {
        Transition assign = new Transition();
        assign.setSourceStates(List.of(State.of("OPEN", "com.example.TicketState.OPEN")));
        assign.setTargetStates(List.of(State.of("ASSIGNED", "com.example.TicketState.ASSIGNED")));
        assign.setEvent(Event.of("ASSIGN", "com.example.TicketEvent.ASSIGN"));

        // /{id}/assign has a path placeholder, but it is a resource id — not an unbound event.
        CallChain chain = CallChain.builder()
                .entryPoint(EntryPoint.builder()
                        .type(EntryPoint.Type.REST)
                        .name("POST /api/ticket/{id}/assign")
                        .className("com.example.WebController")
                        .methodName("assignTicket")
                        .parameters(List.of(
                                EntryPoint.Parameter.builder()
                                        .name("id")
                                        .type("String")
                                        .annotations(List.of("PathVariable"))
                                        .build(),
                                EntryPoint.Parameter.builder()
                                        .name("userId")
                                        .type("String")
                                        .annotations(List.of("RequestParam"))
                                        .build()))
                        .build())
                .triggerPoint(TriggerPoint.builder()
                        .event("com.example.TicketEvent.ASSIGN")
                        .polymorphicEvents(List.of("com.example.TicketEvent.ASSIGN"))
                        .external(true)
                        .build())
                .build();

        AnalysisResult result = AnalysisResult.builder()
                .name("TicketStateMachineConfig")
                .eventTypeFqn("com.example.TicketEvent")
                .transitions(List.of(assign))
                .metadata(CodebaseMetadata.builder().callChains(List.of(chain)).build())
                .build();

        enricher.enrich(result, null, null);

        CallChain updated = result.getMetadata().getCallChains().get(0);
        assertThat(updated.getLinkResolution()).isEqualTo(LinkResolution.RESOLVED);
        assertThat(updated.getMatchedTransitions()).hasSize(1);
        assertThat(updated.getTriggerPoint().isExternal()).isFalse();
    }

    @Test
    void shouldStillFailClosedOnUnboundEventPathTemplate() {
        Transition payT = new Transition();
        payT.setSourceStates(List.of(State.of("NEW", "OrderState.NEW")));
        payT.setTargetStates(List.of(State.of("PAID", "OrderState.PAID")));
        payT.setEvent(Event.of("OrderEvent.PAY", "com.example.OrderEvent.PAY"));

        CallChain chain = CallChain.builder()
                .entryPoint(EntryPoint.builder()
                        .type(EntryPoint.Type.REST)
                        .name("POST /api/machine/{machineType}/transition/{event}")
                        .className("com.example.Api")
                        .methodName("transition")
                        .build())
                .triggerPoint(TriggerPoint.builder()
                        .event("OrderEvent.valueOf(eventStr)")
                        .external(true)
                        .ambiguous(true)
                        .build())
                .build();

        AnalysisResult result = AnalysisResult.builder()
                .name("OrderStateMachineConfig")
                .transitions(List.of(payT))
                .metadata(CodebaseMetadata.builder().callChains(List.of(chain)).build())
                .build();

        enricher.enrich(result, null, null);

        CallChain updated = result.getMetadata().getCallChains().get(0);
        assertThat(updated.getLinkResolution()).isEqualTo(LinkResolution.UNRESOLVED_EXTERNAL);
        assertThat(updated.getMatchedTransitions()).isNullOrEmpty();
    }

    @Test
    void shouldLinkWhenSymbolicPolymorphicEventsExpandToMachineEnumConstants() {
        Transition payT = new Transition();
        payT.setSourceStates(List.of(
                State.of("OrderState.NEW", "com.example.order.OrderState.NEW")));
        payT.setTargetStates(List.of(
                State.of("OrderState.PAID", "com.example.order.OrderState.PAID")));
        payT.setEvent(Event.of("OrderEvent.PAY", "com.example.order.OrderEvent.PAY"));

        CallChain chain = CallChain.builder()
                .triggerPoint(TriggerPoint.builder()
                        .event("eventString")
                        .polymorphicEvents(List.of("com.example.order.OrderEvent.PAY"))
                        .constraint("\"ORDER\".equalsIgnoreCase(machineType)")
                        .build())
                .build();

        AnalysisResult result = AnalysisResult.builder()
                .name("com.example.config.OrderStateMachineConfiguration")
                .eventTypeFqn("com.example.order.OrderEvent")
                .transitions(List.of(payT))
                .metadata(CodebaseMetadata.builder().callChains(List.of(chain)).build())
                .build();

        enricher.enrich(result, null, null);

        assertThat(result.getMetadata().getCallChains().get(0).getMatchedTransitions()).hasSize(1);
    }

    @Test
    void shouldRespectDispatcherBranchConstraintForMachineDomain() {
        Transition payT = new Transition();
        payT.setSourceStates(List.of(State.of("NEW", "OrderState.NEW")));
        payT.setTargetStates(List.of(State.of("PAID", "OrderState.PAID")));
        payT.setEvent(Event.of("OrderEvent.PAY", "com.example.OrderEvent.PAY"));

        CallChain chain = CallChain.builder()
                .triggerPoint(TriggerPoint.builder()
                        .event("OrderEvent.PAY")
                        .polymorphicEvents(List.of("OrderEvent.PAY"))
                        .constraint("\"ORDER\".equalsIgnoreCase(type)")
                        .build())
                .build();

        AnalysisResult result = AnalysisResult.builder()
                .name("com.example.OrderStateMachineConfig")
                .transitions(List.of(payT))
                .metadata(CodebaseMetadata.builder().callChains(List.of(chain)).build())
                .build();

        enricher.enrich(result, null, null);

        assertThat(result.getMetadata().getCallChains().get(0).getMatchedTransitions()).hasSize(1);
    }

    @Test
    void shouldLinkTransitionByEventOnly() {
        Transition t1 = new Transition();
        t1.setSourceStates(List.of(State.of("NEW", "NEW")));
        t1.setTargetStates(List.of(State.of("PAID", "PAID")));
        t1.setEvent(Event.of("PAY", "PAY"));

        Transition t2 = new Transition();
        t2.setSourceStates(List.of(State.of("FAILED", "FAILED")));
        t2.setTargetStates(List.of(State.of("PAID", "PAID")));
        t2.setEvent(Event.of("PAY", "PAY"));

        CallChain chain = CallChain.builder()
                .triggerPoint(TriggerPoint.builder().event("PAY").build())
                .build();

        AnalysisResult result = AnalysisResult.builder()
                .transitions(List.of(t1, t2))
                .metadata(CodebaseMetadata.builder().callChains(List.of(chain)).build())
                .build();

        enricher.enrich(result, null, null);

        CallChain updatedChain = result.getMetadata().getCallChains().get(0);
        // Same event from multiple places — link all (baseline / 33ccc4a behavior).
        assertThat(updatedChain.getMatchedTransitions()).hasSize(2);
        assertThat(updatedChain.getLinkResolution()).isEqualTo(LinkResolution.RESOLVED);
        assertThat(updatedChain.getTriggerPoint().isAmbiguous()).isFalse();
    }

    @Test
    void shouldLinkTransitionByEventWhenSourceStateIsUniquelyDeterminedByTransitionTable() {
        Transition t1 = new Transition();
        t1.setSourceStates(List.of(State.of("NEW", "NEW")));
        t1.setTargetStates(List.of(State.of("PAID", "PAID")));
        t1.setEvent(Event.of("PAY", "PAY"));

        CallChain chain = CallChain.builder()
                .triggerPoint(TriggerPoint.builder().event("PAY").build())
                .build();

        AnalysisResult result = AnalysisResult.builder()
                .transitions(List.of(t1))
                .metadata(CodebaseMetadata.builder().callChains(List.of(chain)).build())
                .build();

        enricher.enrich(result, null, null);

        CallChain updatedChain = result.getMetadata().getCallChains().get(0);
        assertThat(updatedChain.getMatchedTransitions()).hasSize(1);
        assertThat(updatedChain.getMatchedTransitions().get(0).getSourceState()).isEqualTo("NEW");
        assertThat(updatedChain.getMatchedTransitions().get(0).getTargetState()).isEqualTo("PAID");
        assertThat(updatedChain.getTriggerPoint().getSourceState()).isEqualTo("NEW");
        assertThat(updatedChain.getTriggerPoint().isAmbiguous()).isFalse();
    }

    @Test
    void shouldLinkWhenSameEventAppearsFromMultipleSourcesWithinSameStateEnum() {
        Transition payFromNew = new Transition();
        payFromNew.setSourceStates(List.of(State.of("NEW", "com.example.order.OrderState.NEW")));
        payFromNew.setTargetStates(List.of(State.of("PAID", "com.example.order.OrderState.PAID")));
        payFromNew.setEvent(Event.of("PAY", "com.example.order.OrderEvent.PAY"));

        Transition payFromPending = new Transition();
        payFromPending.setSourceStates(List.of(State.of("PENDING", "com.example.order.OrderState.PENDING")));
        payFromPending.setTargetStates(List.of(State.of("PAID", "com.example.order.OrderState.PAID")));
        payFromPending.setEvent(Event.of("PAY", "com.example.order.OrderEvent.PAY"));

        // Known concrete event, no endpoint-narrow poly list required for multi-source linking.
        CallChain chain = CallChain.builder()
                .triggerPoint(TriggerPoint.builder()
                        .event("com.example.order.OrderEvent.PAY")
                        .build())
                .build();

        AnalysisResult result = AnalysisResult.builder()
                .name("com.example.config.OrderStateMachineConfiguration")
                .eventTypeFqn("com.example.order.OrderEvent")
                .stateTypeFqn("com.example.order.OrderState")
                .transitions(List.of(payFromNew, payFromPending))
                .metadata(CodebaseMetadata.builder().callChains(List.of(chain)).build())
                .build();

        enricher.enrich(result, null, null);

        CallChain updatedChain = result.getMetadata().getCallChains().get(0);
        assertThat(updatedChain.getMatchedTransitions()).hasSize(2);
        assertThat(updatedChain.getLinkResolution()).isEqualTo(LinkResolution.RESOLVED);
    }

    @Test
    void shouldNotLinkAllTransitionsWhenPolyContainsManyEnumConstants() {
        Transition pay = new Transition();
        pay.setSourceStates(List.of(State.of("NEW", "com.example.order.OrderState.NEW")));
        pay.setTargetStates(List.of(State.of("PAID", "com.example.order.OrderState.PAID")));
        pay.setEvent(Event.of("PAY", "com.example.order.OrderEvent.PAY"));

        Transition ship = new Transition();
        ship.setSourceStates(List.of(State.of("PAID", "com.example.order.OrderState.PAID")));
        ship.setTargetStates(List.of(State.of("SHIPPED", "com.example.order.OrderState.SHIPPED")));
        ship.setEvent(Event.of("SHIP", "com.example.order.OrderEvent.SHIP"));

        Transition cancel = new Transition();
        cancel.setSourceStates(List.of(State.of("NEW", "com.example.order.OrderState.NEW")));
        cancel.setTargetStates(List.of(State.of("CANCELLED", "com.example.order.OrderState.CANCELLED")));
        cancel.setEvent(Event.of("CANCEL", "com.example.order.OrderEvent.CANCEL"));

        CallChain chain = CallChain.builder()
                .triggerPoint(TriggerPoint.builder()
                        .ambiguous(true)
                        .event("OrderEvent.valueOf(eventStr)")
                        .polymorphicEvents(List.of(
                                "com.example.order.OrderEvent.PAY",
                                "com.example.order.OrderEvent.SHIP",
                                "com.example.order.OrderEvent.CANCEL"))
                        .build())
                .build();

        AnalysisResult result = AnalysisResult.builder()
                .name("com.example.config.OrderStateMachineConfiguration")
                .eventTypeFqn("com.example.order.OrderEvent")
                .stateTypeFqn("com.example.order.OrderState")
                .transitions(List.of(pay, ship, cancel))
                .metadata(CodebaseMetadata.builder().callChains(List.of(chain)).build())
                .build();

        enricher.enrich(result, null, null);

        CallChain updatedChain = result.getMetadata().getCallChains().get(0);
        assertThat(updatedChain.getMatchedTransitions()).isNullOrEmpty();
        assertThat(updatedChain.getLinkResolution()).isEqualTo(LinkResolution.AMBIGUOUS_WIDEN);
    }

    @Test
    void shouldNotInferSourceStateWhenDifferentEnumsShareSameConstantName() {
        Transition t1 = new Transition();
        t1.setSourceStates(List.of(State.of("NEW", "com.example.order.OrderState.NEW")));
        t1.setTargetStates(List.of(State.of("PAID", "com.example.order.OrderState.PAID")));
        t1.setEvent(Event.of("PAY", "PAY"));

        Transition t2 = new Transition();
        t2.setSourceStates(List.of(State.of("NEW", "com.example.invoice.InvoiceState.NEW")));
        t2.setTargetStates(List.of(State.of("PAID", "com.example.invoice.InvoiceState.PAID")));
        t2.setEvent(Event.of("PAY", "PAY"));

        CallChain chain = CallChain.builder()
                .triggerPoint(TriggerPoint.builder().event("PAY").build())
                .build();

        AnalysisResult result = AnalysisResult.builder()
                .transitions(List.of(t1, t2))
                .metadata(CodebaseMetadata.builder().callChains(List.of(chain)).build())
                .build();

        enricher.enrich(result, null, null);

        CallChain updatedChain = result.getMetadata().getCallChains().get(0);
        assertThat(updatedChain.getTriggerPoint().getSourceState()).isNull();
        assertThat(updatedChain.getTriggerPoint().isAmbiguous()).isTrue();
        assertThat(updatedChain.getMatchedTransitions()).isNullOrEmpty();
        assertThat(updatedChain.getLinkResolution()).isEqualTo(click.kamil.springstatemachineexporter.analysis.model.LinkResolution.AMBIGUOUS_WIDEN);
    }

    @Test
    void shouldLinkTransitionByEventAndSourceState() {
        Transition t1 = new Transition();
        t1.setSourceStates(List.of(State.of("NEW", "NEW")));
        t1.setTargetStates(List.of(State.of("PAID", "PAID")));
        t1.setEvent(Event.of("PAY", "PAY"));

        Transition t2 = new Transition();
        t2.setSourceStates(List.of(State.of("FAILED", "FAILED")));
        t2.setTargetStates(List.of(State.of("PAID", "PAID")));
        t2.setEvent(Event.of("PAY", "PAY"));

        CallChain chain = CallChain.builder()
                .triggerPoint(TriggerPoint.builder().event("PAY").sourceState("NEW").build())
                .build();

        AnalysisResult result = AnalysisResult.builder()
                .transitions(List.of(t1, t2))
                .metadata(CodebaseMetadata.builder().callChains(List.of(chain)).build())
                .build();

        enricher.enrich(result, null, null);

        CallChain updatedChain = result.getMetadata().getCallChains().get(0);
        assertThat(updatedChain.getMatchedTransitions()).hasSize(1);
        assertThat(updatedChain.getMatchedTransitions().get(0).getSourceState()).isEqualTo("NEW");
        assertThat(updatedChain.getMatchedTransitions().get(0).getTargetState()).isEqualTo("PAID");
    }

    @Test
    void shouldLinkTransitionByFullIdentifier() {
        Transition t1 = new Transition();
        t1.setSourceStates(List.of(State.of("\"NEW\"", "NEW")));
        t1.setTargetStates(List.of(State.of("\"PAID\"", "PAID")));
        // This simulates how Spring Config usually has Event as OrderEvents.PAY but its resolved string is "PAY_ORDER"
        t1.setEvent(Event.of("OrderEvents.PAY", "PAY_ORDER"));

        CallChain chain = CallChain.builder()
                .triggerPoint(TriggerPoint.builder().event("PAY_ORDER").sourceState("NEW").build())
                .build();

        AnalysisResult result = AnalysisResult.builder()
                .transitions(List.of(t1))
                .metadata(CodebaseMetadata.builder().callChains(List.of(chain)).build())
                .build();

        enricher.enrich(result, null, null);

        CallChain updatedChain = result.getMetadata().getCallChains().get(0);
        assertThat(updatedChain.getMatchedTransitions()).hasSize(1);
        assertThat(updatedChain.getMatchedTransitions().get(0).getSourceState()).isEqualTo("NEW");
        assertThat(updatedChain.getMatchedTransitions().get(0).getTargetState()).isEqualTo("PAID");
        assertThat(updatedChain.getMatchedTransitions().get(0).getEvent()).isEqualTo("PAY_ORDER");
    }

    @Test
    void shouldHandleUnknownEvents() {
        CallChain chain = CallChain.builder()
                .triggerPoint(TriggerPoint.builder().event(null).build()) // Dynamic or unresolved event
                .build();

        AnalysisResult result = AnalysisResult.builder()
                .transitions(List.of(new Transition()))
                .metadata(CodebaseMetadata.builder().callChains(List.of(chain)).build())
                .build();

        enricher.enrich(result, null, null);

        CallChain updatedChain = result.getMetadata().getCallChains().get(0);
        assertThat(updatedChain.getMatchedTransitions()).isNull();
    }

    @Test
    void shouldMatchWildcardVariable() {
        Transition t1 = new Transition();
        t1.setSourceStates(List.of(State.of("NEW", "NEW")));
        t1.setTargetStates(List.of(State.of("PAID", "PAID")));
        t1.setEvent(Event.of("PAY", "PAY"));

        CallChain chain = CallChain.builder()
                .triggerPoint(TriggerPoint.builder().event("event").build())
                .contextMachineId("testMachine")
                .build();

        AnalysisResult result = AnalysisResult.builder()
                .name("testMachine")
                .transitions(List.of(t1))
                .metadata(CodebaseMetadata.builder().callChains(List.of(chain)).build())
                .build();

        enricher.enrich(result, null, null);

        CallChain updatedChain = result.getMetadata().getCallChains().get(0);
        assertThat(updatedChain.getMatchedTransitions()).hasSize(1);
    }

    @Test
    void shouldNotLinkAllEnumTransitionsWhenConstantNamesCollide() {
        Transition orderPay = new Transition();
        orderPay.setSourceStates(List.of(State.of("NEW", "NEW")));
        orderPay.setTargetStates(List.of(State.of("PAID", "PAID")));
        orderPay.setEvent(Event.of("PAY", "com.example.OrderEvents.PAY"));

        Transition invoicePay = new Transition();
        invoicePay.setSourceStates(List.of(State.of("DRAFT", "DRAFT")));
        invoicePay.setTargetStates(List.of(State.of("SENT", "SENT")));
        invoicePay.setEvent(Event.of("PAY", "com.example.InvoiceEvents.PAY"));

        CallChain chain = CallChain.builder()
                .triggerPoint(TriggerPoint.builder()
                        .event("OrderEvents.PAY")
                        .eventTypeFqn("com.example.OrderEvents")
                        .build())
                .contextMachineId("testMachine")
                .build();

        AnalysisResult result = AnalysisResult.builder()
                .name("testMachine")
                .transitions(List.of(orderPay, invoicePay))
                .metadata(CodebaseMetadata.builder().callChains(List.of(chain)).build())
                .build();

        enricher.enrich(result, null, null);

        CallChain updatedChain = result.getMetadata().getCallChains().get(0);
        assertThat(updatedChain.getMatchedTransitions()).hasSize(1);
        assertThat(updatedChain.getMatchedTransitions().get(0).getEvent()).isEqualTo("com.example.OrderEvents.PAY");
    }

    @Test
    void shouldMatchBaseEventDespiteScrapedPolyEvents() {
        Transition t1 = new Transition();
        t1.setSourceStates(List.of(State.of("NEW", "NEW")));
        t1.setTargetStates(List.of(State.of("PAID", "PAID")));
        // State machine uses the base event generically
        t1.setEvent(Event.of("BaseEvent", "BaseEvent"));

        // Trigger point uses "BaseEvent", but polyEvents scraped some implementation/constants
        CallChain chain = CallChain.builder()
                .triggerPoint(TriggerPoint.builder()
                        .event("BaseEvent")
                        .polymorphicEvents(List.of("EVENT_A", "EVENT_B"))
                        .build())
                .contextMachineId("testMachine")
                .build();

        AnalysisResult result = AnalysisResult.builder()
                .name("testMachine")
                .transitions(List.of(t1))
                .metadata(CodebaseMetadata.builder().callChains(List.of(chain)).build())
                .build();

        enricher.enrich(result, null, null);

        // It should match because smEvent matches triggerEvent directly, overriding the failed poly search
        CallChain updatedChain = result.getMetadata().getCallChains().get(0);
        assertThat(updatedChain.getMatchedTransitions()).hasSize(1);
    }

    @Test
    void shouldNotMatchArbitraryGetXMethodWildcard() {
        Transition t1 = new Transition();
        t1.setSourceStates(List.of(State.of("NEW", "NEW")));
        t1.setTargetStates(List.of(State.of("PAID", "PAID")));
        t1.setEvent(Event.of("PAY", "PAY"));

        CallChain chain = CallChain.builder()
                .triggerPoint(TriggerPoint.builder().event("richEvent.getId()").build())
                .contextMachineId("testMachine")
                .build();

        AnalysisResult result = AnalysisResult.builder()
                .name("testMachine")
                .transitions(List.of(t1))
                .metadata(CodebaseMetadata.builder().callChains(List.of(chain)).build())
                .build();

        enricher.enrich(result, null, null);

        CallChain updatedChain = result.getMetadata().getCallChains().get(0);
        assertThat(updatedChain.getMatchedTransitions()).isNullOrEmpty();
    }

    @Test
    void shouldAllowWildcardWhenContextMachineIdIsMissing() {
        Transition t1 = new Transition();
        t1.setSourceStates(List.of(State.of("NEW", "NEW")));
        t1.setTargetStates(List.of(State.of("PAID", "PAID")));
        t1.setEvent(Event.of("PAY", "PAY"));

        CallChain chain = CallChain.builder()
                .triggerPoint(TriggerPoint.builder().event("event").build())
                // NO contextMachineId or stateMachineId
                .build();

        AnalysisResult result = AnalysisResult.builder()
                .name("testMachine")
                .transitions(List.of(t1))
                .metadata(CodebaseMetadata.builder().callChains(List.of(chain)).build())
                .build();

        enricher.enrich(result, null, null);

        CallChain updatedChain = result.getMetadata().getCallChains().get(0);
        assertThat(updatedChain.getMatchedTransitions()).hasSize(1);
    }

    @Test
    void shouldNotFalsePositiveOnSubstringContains() {
        // This test proves that the heuristic is entirely removed and false positives are eliminated
        Transition t1 = new Transition();
        t1.setSourceStates(List.of(State.of("NEW", "NEW")));
        t1.setTargetStates(List.of(State.of("PAID", "PAID")));
        t1.setEvent(Event.of("PAY", "PAY"));

        // Trigger point sends "PAYMENT" which contains "PAY". Under the old heuristics, this would falsely match!
        CallChain chain = CallChain.builder()
                .triggerPoint(TriggerPoint.builder().event("PAYMENT").build())
                .build();

        AnalysisResult result = AnalysisResult.builder()
                .transitions(List.of(t1))
                .metadata(CodebaseMetadata.builder().callChains(List.of(chain)).build())
                .build();

        enricher.enrich(result, null, null);

        // It should NOT match, because "PAYMENT" != "PAY" strictly.
        CallChain updatedChain = result.getMetadata().getCallChains().get(0);
        assertThat(updatedChain.getMatchedTransitions()).isNullOrEmpty();
    }

    @Test
    void shouldExecuteDeterministicallyAndUtilizeInternalCaches() {
        Transition t1 = new Transition();
        t1.setSourceStates(List.of(State.of("NEW", "NEW")));
        t1.setTargetStates(List.of(State.of("PAID", "PAID")));
        t1.setEvent(Event.of("PAY", "PAY"));

        CallChain chain1 = CallChain.builder().triggerPoint(TriggerPoint.builder().event("PAY").build()).build();
        CallChain chain2 = CallChain.builder().triggerPoint(TriggerPoint.builder().event("PAY").build()).build();
        CallChain chain3 = CallChain.builder().triggerPoint(TriggerPoint.builder().event("PAY").build()).build();

        AnalysisResult result = AnalysisResult.builder()
                .transitions(List.of(t1))
                .metadata(CodebaseMetadata.builder().callChains(List.of(chain1, chain2, chain3)).build())
                .build();

        enricher.enrich(result, null, null);

        // All 3 identical trigger point chains should be linked seamlessly using the memoization cache.
        for (CallChain chain : result.getMetadata().getCallChains()) {
            assertThat(chain.getMatchedTransitions()).hasSize(1);
            assertThat(chain.getMatchedTransitions().get(0).getEvent()).isEqualTo("PAY");
        }
    }


    @Test
    void shouldFilterCrossStateMachineRoutingByVertical() {
        Transition t1 = new Transition();
        t1.setSourceStates(List.of(State.of("NEW", "NEW")));
        t1.setTargetStates(List.of(State.of("ASSEMBLED", "ASSEMBLED")));
        t1.setEvent(Event.of("ASSEMBLE", "ASSEMBLE"));

        CallChain chain = CallChain.builder()
                .triggerPoint(TriggerPoint.builder().event("ASSEMBLE").build())
                .methodChain(List.of(
                        "com.example.electronics.ElectronicsOrderController.post()",
                        "com.example.electronics.ElectronicsOrderService.processOrderEvent()"
                ))
                .build();

        // Testing against Electronics SM - should match
        AnalysisResult resultElectronics = AnalysisResult.builder()
                .name("com.example.electronics.ElectronicsOwnDeliveryStateMachineConfiguration")
                .transitions(List.of(t1))
                .metadata(CodebaseMetadata.builder().callChains(List.of(chain)).build())
                .build();

        enricher.enrich(resultElectronics, null, null);
        CallChain updatedChainElec = resultElectronics.getMetadata().getCallChains().get(0);
        assertThat(updatedChainElec.getMatchedTransitions()).hasSize(1);

        // Testing against ComputerStore SM - should NOT match because chain has Electronics service
        AnalysisResult resultComputer = AnalysisResult.builder()
                .name("com.example.computerstore.OwnDeliveryStateMachineConfiguration") // the non-electronics one
                .transitions(List.of(t1))
                .metadata(CodebaseMetadata.builder().callChains(List.of(chain)).build())
                .build();

        enricher.enrich(resultComputer, null, null);
        assertThat(resultComputer.getMetadata().getCallChains()).isEmpty();
    }


    @Test
    void shouldAllowSharedServicesToRouteToMultipleStateMachines() {
        Transition t1 = new Transition();
        t1.setSourceStates(List.of(State.of("NEW", "NEW")));
        t1.setTargetStates(List.of(State.of("PROCESSED", "PROCESSED")));
        t1.setEvent(Event.of("PROCESS", "PROCESS"));

        // Chain contains NO vertical prefix (e.g. CommonOrderService)
        CallChain chain = CallChain.builder()
                .triggerPoint(TriggerPoint.builder().event("PROCESS").build())
                .methodChain(List.of(
                        "com.example.CommonOrderController.post()",
                        "com.example.CommonOrderService.processOrderEvent()"
                ))
                .build();

        AnalysisResult resultElectronics = AnalysisResult.builder()
                .name("com.example.electronics.ElectronicsOwnDeliveryStateMachineConfiguration")
                .transitions(List.of(t1))
                .metadata(CodebaseMetadata.builder().callChains(List.of(chain)).build())
                .build();

        enricher.enrich(resultElectronics, null, null);
        CallChain updatedChainElec = resultElectronics.getMetadata().getCallChains().get(0);
        
        // Should match Electronics because it's a shared service (no computerstore service in path)
        assertThat(updatedChainElec.getMatchedTransitions()).hasSize(1);

        AnalysisResult resultComputer = AnalysisResult.builder()
                .name("com.example.computerstore.OwnDeliveryStateMachineConfiguration")
                .transitions(List.of(t1))
                .metadata(CodebaseMetadata.builder().callChains(List.of(chain)).build())
                .build();

        enricher.enrich(resultComputer, null, null);
        CallChain updatedChainComp = resultComputer.getMetadata().getCallChains().get(0);
        
        // Should ALSO match ComputerStore because it's a shared service
        assertThat(updatedChainComp.getMatchedTransitions()).hasSize(1);
    }


    @Test
    void shouldFilterCrossStateMachineRoutingWithMultipleVerticals() {
        Transition t1 = new Transition();
        t1.setSourceStates(List.of(State.of("NEW", "NEW")));
        t1.setTargetStates(List.of(State.of("ASSEMBLED", "ASSEMBLED")));
        t1.setEvent(Event.of("ASSEMBLE", "ASSEMBLE"));

        // Chain 1: Electronics
        CallChain chainElec = CallChain.builder()
                .triggerPoint(TriggerPoint.builder().event("ASSEMBLE").build())
                .methodChain(List.of("com.example.electronics.ElectronicsOrderController.post()"))
                .build();
                
        // Chain 2: Furniture
        CallChain chainFurn = CallChain.builder()
                .triggerPoint(TriggerPoint.builder().event("ASSEMBLE").build())
                .methodChain(List.of("com.example.furniture.FurnitureOrderController.post()"))
                .build();
                
        // Chain 3: Groceries
        CallChain chainGroc = CallChain.builder()
                .triggerPoint(TriggerPoint.builder().event("ASSEMBLE").build())
                .methodChain(List.of("com.example.groceries.GroceriesOrderController.post()"))
                .build();

        // SM 1: Electronics
        AnalysisResult resElec = AnalysisResult.builder()
                .name("com.example.electronics.ElectronicsOwnDeliveryStateMachineConfiguration")
                .transitions(List.of(t1))
                .metadata(CodebaseMetadata.builder().callChains(List.of(chainElec, chainFurn, chainGroc)).build())
                .build();
                
        // SM 2: Furniture
        AnalysisResult resFurn = AnalysisResult.builder()
                .name("com.example.furniture.FurnitureOwnDeliveryStateMachineConfiguration")
                .transitions(List.of(t1))
                .metadata(CodebaseMetadata.builder().callChains(List.of(chainElec, chainFurn, chainGroc)).build())
                .build();
                
        // SM 3: Groceries
        AnalysisResult resGroc = AnalysisResult.builder()
                .name("com.example.groceries.GroceriesStateMachineConfiguration")
                .transitions(List.of(t1))
                .metadata(CodebaseMetadata.builder().callChains(List.of(chainElec, chainFurn, chainGroc)).build())
                .build();
                
        // SM 4: Computer Store (The catch-all base one in computerstore package)
        AnalysisResult resComp = AnalysisResult.builder()
                .name("com.example.computerstore.OwnDeliveryStateMachineConfiguration")
                .transitions(List.of(t1))
                .metadata(CodebaseMetadata.builder().callChains(List.of(chainElec, chainFurn, chainGroc)).build())
                .build();

        // Test Electronics SM - Should only keep Electronics Chain
        enricher.enrich(resElec, null, null);
        assertThat(resElec.getMetadata().getCallChains()).hasSize(1);
        assertThat(resElec.getMetadata().getCallChains().get(0).getMatchedTransitions()).hasSize(1);

        // Test Furniture SM - Should only keep Furniture Chain
        enricher.enrich(resFurn, null, null);
        assertThat(resFurn.getMetadata().getCallChains()).hasSize(1);
        assertThat(resFurn.getMetadata().getCallChains().get(0).getMatchedTransitions()).hasSize(1);
        
        // Test Groceries SM - Should only keep Groceries Chain
        enricher.enrich(resGroc, null, null);
        assertThat(resGroc.getMetadata().getCallChains()).hasSize(1);
        assertThat(resGroc.getMetadata().getCallChains().get(0).getMatchedTransitions()).hasSize(1);
        
        // Test ComputerStore SM - Should reject ALL because none belong to computerstore
        enricher.enrich(resComp, null, null);
        assertThat(resComp.getMetadata().getCallChains()).isEmpty();
    }

    @Test
    void shouldHandleMultipleNegations() {
        Transition t1 = new Transition();
        t1.setSourceStates(List.of(State.of("NEW", "NEW")));
        t1.setTargetStates(List.of(State.of("PAID", "PAID")));
        t1.setEvent(Event.of("PAY", "PAY"));

        // Case 1: Double Negation on ORDER -> !(!(ORDER)) -> Should act as POSITIVE (meaning order is required).
        // Since we are running OrderStateMachineConfiguration, it should match!
        CallChain chain1 = CallChain.builder()
                .triggerPoint(TriggerPoint.builder()
                        .event("PAY")
                        .constraint("!(!(ORDER.equals(type)))")
                        .build())
                .build();

        AnalysisResult resultOrder = AnalysisResult.builder()
                .name("com.example.OrderStateMachineConfiguration")
                .transitions(List.of(t1))
                .metadata(CodebaseMetadata.builder().callChains(List.of(chain1)).build())
                .build();

        enricher.enrich(resultOrder, null, null);
        assertThat(resultOrder.getMetadata().getCallChains().get(0).getMatchedTransitions()).hasSize(1);

        // Case 2: Triple Negation on ORDER -> !(!(!(ORDER))) -> Should act as NEGATIVE (meaning order is rejected/forbidden).
        // Under OrderStateMachineConfiguration, this chain should be rejected!
        CallChain chain2 = CallChain.builder()
                .triggerPoint(TriggerPoint.builder()
                        .event("PAY")
                        .constraint("!(!(!(ORDER.equals(type))))")
                        .build())
                .build();

        AnalysisResult resultOrder2 = AnalysisResult.builder()
                .name("com.example.OrderStateMachineConfiguration")
                .transitions(List.of(t1))
                .metadata(CodebaseMetadata.builder().callChains(List.of(chain2)).build())
                .build();

        enricher.enrich(resultOrder2, null, null);
        assertThat(resultOrder2.getMetadata().getCallChains().get(0).getMatchedTransitions()).isNullOrEmpty();
    }

    @Test
    void shouldHandleComplexBooleanLogic() {
        Transition t1 = new Transition();
        t1.setSourceStates(List.of(State.of("NEW", "NEW")));
        t1.setTargetStates(List.of(State.of("PAID", "PAID")));
        t1.setEvent(Event.of("PAY", "PAY"));

        // Case 1: OR expression -> "ORDER".equals(type) || "DOCUMENT".equals(type)
        // Under OrderStateMachineConfiguration, it should match!
        CallChain chain1 = CallChain.builder()
                .triggerPoint(TriggerPoint.builder()
                        .event("PAY")
                        .constraint("\"ORDER\".equalsIgnoreCase(type) || \"DOCUMENT\".equalsIgnoreCase(type)")
                        .build())
                .build();

        AnalysisResult resultOrder = AnalysisResult.builder()
                .name("com.example.OrderStateMachineConfiguration")
                .transitions(List.of(t1))
                .metadata(CodebaseMetadata.builder().callChains(List.of(chain1)).build())
                .build();

        enricher.enrich(resultOrder, null, null);
        assertThat(resultOrder.getMetadata().getCallChains().get(0).getMatchedTransitions()).hasSize(1);

        // Case 2: Under UserStateMachineConfiguration, it should be rejected!
        AnalysisResult resultUser = AnalysisResult.builder()
                .name("com.example.UserStateMachineConfiguration")
                .transitions(List.of(t1))
                .metadata(CodebaseMetadata.builder().callChains(List.of(chain1)).build())
                .build();

        enricher.enrich(resultUser, null, null);
        assertThat(resultUser.getMetadata().getCallChains().get(0).getMatchedTransitions()).isNullOrEmpty();
    }

    @Test
    void shouldDeduplicateSemanticallyEquivalentChainsKeepingShortestPath() {
        Transition payT = new Transition();
        payT.setSourceStates(List.of(State.of("NEW", "OrderState.NEW")));
        payT.setTargetStates(List.of(State.of("PAID", "OrderState.PAID")));
        payT.setEvent(Event.of("OrderEvent.PAY", "com.example.OrderEvent.PAY"));

        EntryPoint entryPoint = EntryPoint.builder()
                .type(EntryPoint.Type.REST)
                .name("POST /api/order/pay")
                .className("com.example.Api")
                .methodName("pay")
                .build();
        TriggerPoint trigger = TriggerPoint.builder()
                .event("OrderEvent.PAY")
                .polymorphicEvents(List.of("OrderEvent.PAY"))
                .build();

        CallChain longPath = CallChain.builder()
                .entryPoint(entryPoint)
                .triggerPoint(trigger)
                .methodChain(List.of(
                        "com.example.Api.pay",
                        "com.example.OrderService.route",
                        "com.example.OrderService.delegate",
                        "com.example.StateMachine.sendEvent"))
                .build();
        CallChain shortPath = CallChain.builder()
                .entryPoint(entryPoint)
                .triggerPoint(trigger)
                .methodChain(List.of(
                        "com.example.Api.pay",
                        "com.example.StateMachine.sendEvent"))
                .build();

        AnalysisResult result = AnalysisResult.builder()
                .name("OrderStateMachineConfig")
                .transitions(List.of(payT))
                .metadata(CodebaseMetadata.builder().callChains(List.of(longPath, shortPath)).build())
                .build();

        enricher.enrich(result, null, null);

        assertThat(result.getMetadata().getCallChains()).hasSize(1);
        assertThat(result.getMetadata().getCallChains().get(0).getMethodChain())
                .containsExactly(
                        "com.example.Api.pay",
                        "com.example.StateMachine.sendEvent");
        assertThat(result.getMetadata().getCallChains().get(0).getMatchedTransitions()).hasSize(1);
    }

    @Test
    void shouldNotStampNonConcreteStringMachineTypeOntoTrigger() {
        Transition orderEvent = new Transition();
        orderEvent.setSourceStates(List.of(State.of("BUSY", "String.BUSY")));
        orderEvent.setTargetStates(List.of(State.of("DONE", "String.DONE")));
        orderEvent.setEvent(Event.of("ORDER_EVENT", "String.ORDER_EVENT"));

        CallChain chain = CallChain.builder()
                .entryPoint(EntryPoint.builder()
                        .type(EntryPoint.Type.JMS)
                        .name("JMS: order.queue")
                        .className("click.kamil.maven.api.JmsOrderListener")
                        .methodName("onMessage")
                        .build())
                .triggerPoint(TriggerPoint.builder()
                        .event("ORDER_EVENT")
                        .polymorphicEvents(List.of("ORDER_EVENT"))
                        .build())
                .methodChain(List.of(
                        "click.kamil.maven.core.MavenOrderStateMachine.OrderService.onMessage"))
                .build();

        AnalysisResult result = AnalysisResult.builder()
                .name("click.kamil.maven.core.MavenOrderStateMachine")
                .eventTypeFqn("String")
                .stateTypeFqn("String")
                .transitions(List.of(orderEvent))
                .metadata(CodebaseMetadata.builder()
                        .entryPoints(List.of(chain.getEntryPoint()))
                        .callChains(List.of(chain))
                        .build())
                .build();

        enricher.enrich(result, null, null);

        assertThat(result.getMetadata().getCallChains()).isNotEmpty();
        TriggerPoint linked = result.getMetadata().getCallChains().get(0).getTriggerPoint();
        assertThat(linked.getEventTypeFqn())
                .as("non-concrete machine String must not be stamped onto the trigger")
                .isNull();
        assertThat(linked.getStateTypeFqn()).isNull();
        assertThat(result.getMetadata().getCallChains().get(0).getLinkResolution())
                .isEqualTo(LinkResolution.RESOLVED);
    }

    @Test
    void shouldRejectChainWhenDispatcherDomainConstraintDoesNotMatchMachine() {
        Transition payT = new Transition();
        payT.setSourceStates(List.of(State.of("NEW", "OrderState.NEW")));
        payT.setTargetStates(List.of(State.of("PAID", "OrderState.PAID")));
        payT.setEvent(Event.of("OrderEvent.PAY", "com.example.OrderEvent.PAY"));

        CallChain chain = CallChain.builder()
                .triggerPoint(TriggerPoint.builder()
                        .event("OrderEvent.PAY")
                        .polymorphicEvents(List.of("OrderEvent.PAY"))
                        .constraint("\"PAYMENT\".equalsIgnoreCase(type)")
                        .build())
                .build();

        AnalysisResult result = AnalysisResult.builder()
                .name("com.example.OrderStateMachineConfig")
                .transitions(List.of(payT))
                .metadata(CodebaseMetadata.builder().callChains(List.of(chain)).build())
                .build();

        enricher.enrich(result, null, null);

        assertThat(result.getMetadata().getCallChains().get(0).getMatchedTransitions()).isNullOrEmpty();
    }
}