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.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 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);
        assertThat(updatedChain.getMatchedTransitions()).hasSize(2);
        assertThat(updatedChain.getMatchedTransitions())
                .extracting("sourceState")
                .containsExactlyInAnyOrder("NEW", "FAILED");
    }

    @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("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()).hasSize(1);
    }

    @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);
        CallChain updatedChainComp = resultComputer.getMetadata().getCallChains().get(0);
        assertThat(updatedChainComp.getMatchedTransitions()).isNullOrEmpty();
    }


    @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().get(0).getMatchedTransitions()).hasSize(1); // Elec
        assertThat(resElec.getMetadata().getCallChains().get(1).getMatchedTransitions()).isNullOrEmpty(); // Furn
        assertThat(resElec.getMetadata().getCallChains().get(2).getMatchedTransitions()).isNullOrEmpty(); // Groc

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

    @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();
    }
}