package click.kamil.springstatemachineexporter.analysis.service;

import click.kamil.springstatemachineexporter.analysis.enricher.TransitionLinkerEnricher;
import click.kamil.springstatemachineexporter.analysis.enricher.TriggerCanonicalizationEnricher;
import click.kamil.springstatemachineexporter.analysis.model.*;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
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 org.junit.jupiter.api.io.TempDir;

import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;

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

class DispatcherEndpointTest {

    @Test
    void shouldLinkOnlySpecificEventWhenEndpointPassesLiteralsThroughDispatcher(@TempDir Path tempDir) throws Exception {
        String source = """
            package com.example;
            public class OrderController {
                private Dispatcher dispatcher;
                public void payOrder() { dispatcher.dispatch("ORDER", "PAY"); }
                public void shipOrder() { dispatcher.dispatch("ORDER", "SHIP"); }
            }
            class Dispatcher {
                public void dispatch(String type, String eventStr) {
                    StateMachine sm = new StateMachine();
                    if ("ORDER".equals(type)) {
                        OrderEvent ev = OrderEvent.valueOf(eventStr);
                        sm.sendEvent(ev);
                    }
                }
            }
            enum OrderEvent { PAY, SHIP, CANCEL }
            class StateMachine { public void sendEvent(OrderEvent e) {} }
            """;
        Files.writeString(tempDir.resolve("App.java"), source);
        CodebaseContext context = new CodebaseContext();
        context.scan(tempDir);
        HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);

        EntryPoint payEp = EntryPoint.builder().className("com.example.OrderController").methodName("payOrder").build();
        TriggerPoint trigger = TriggerPoint.builder().className("com.example.StateMachine").methodName("sendEvent").event("e").build();
        CallChain payChain = engine.findChains(List.of(payEp), List.of(trigger)).get(0);
        TriggerPoint resolved = payChain.getTriggerPoint();

        assertThat(resolved.getPolymorphicEvents())
                .as("resolved trigger=%s eventTypeFqn=%s external=%s", resolved.getEvent(), resolved.getEventTypeFqn(), resolved.isExternal())
                .contains("OrderEvent.PAY")
                .doesNotContain("OrderEvent.SHIP", "OrderEvent.CANCEL");

        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"));
        Transition shipT = new Transition();
        shipT.setSourceStates(List.of(State.of("PAID", "OrderState.PAID")));
        shipT.setTargetStates(List.of(State.of("SHIPPED", "OrderState.SHIPPED")));
        shipT.setEvent(Event.of("OrderEvent.SHIP", "com.example.OrderEvent.SHIP"));

        assertThat(new click.kamil.springstatemachineexporter.analysis.enricher.matching.StrictFqnMatchingEngine()
                .matches(payT.getEvent(), resolved)).isTrue();

        AnalysisResult result = AnalysisResult.builder()
                .name("OrderStateMachineConfig")
                .transitions(List.of(payT, shipT))
                .metadata(CodebaseMetadata.builder().callChains(List.of(payChain)).build())
                .build();
        new TransitionLinkerEnricher().enrich(result, null, null);

        assertThat(result.getMetadata().getCallChains().get(0).getMatchedTransitions())
                .hasSize(1)
                .first()
                .extracting("event")
                .isEqualTo("com.example.OrderEvent.PAY");
    }

    @Test
    void shouldLinkTransitionsForGenericDispatchWhenPolymorphicEventsAreExpanded(@TempDir Path tempDir) throws Exception {
        String source = """
            package com.example;
            public class OrderController {
                private Dispatcher dispatcher;
                public void transition(String machineType, String event) {
                    dispatcher.dispatch(machineType, event);
                }
            }
            class Dispatcher {
                public void dispatch(String type, String eventStr) {
                    StateMachine sm = new StateMachine();
                    if ("ORDER".equals(type)) {
                        OrderEvent ev = OrderEvent.valueOf(eventStr);
                        sm.sendEvent(ev);
                    }
                }
            }
            enum OrderEvent { PAY, SHIP }
            enum OrderState { NEW, PAID, SHIPPED }
            class StateMachine { public void sendEvent(OrderEvent e) {} }
            """;
        Files.writeString(tempDir.resolve("App.java"), source);
        Files.createDirectories(tempDir.resolve("com/example/config"));
        Files.writeString(tempDir.resolve("com/example/config/OrderStateMachineConfig.java"),
                """
                package com.example.config;
                import com.example.OrderEvent;
                import com.example.OrderState;
                import org.springframework.statemachine.config.EnumStateMachineConfigurerAdapter;
                public class OrderStateMachineConfig extends EnumStateMachineConfigurerAdapter<OrderState, OrderEvent> {
                }
                """);
        CodebaseContext context = new CodebaseContext();
        context.scan(tempDir);
        HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);

        EntryPoint entry = EntryPoint.builder()
                .className("com.example.OrderController")
                .methodName("transition")
                .build();
        TriggerPoint trigger = TriggerPoint.builder()
                .className("com.example.StateMachine")
                .methodName("sendEvent")
                .event("e")
                .build();
        CallChain chain = engine.findChains(List.of(entry), List.of(trigger)).get(0);

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

        AnalysisResult result = AnalysisResult.builder()
                .name("com.example.config.OrderStateMachineConfig")
                .transitions(List.of(payT, shipT))
                .metadata(CodebaseMetadata.builder().callChains(List.of(chain)).build())
                .build();
        new TriggerCanonicalizationEnricher().enrich(result, context, null);
        new TransitionLinkerEnricher().enrich(result, context, null);

        TriggerPoint linkedTrigger = result.getMetadata().getCallChains().get(0).getTriggerPoint();
        assertThat(linkedTrigger.getPolymorphicEvents())
                .contains("com.example.OrderEvent.PAY", "com.example.OrderEvent.SHIP");
        assertThat(result.getMetadata().getCallChains().get(0).getMatchedTransitions()).isNullOrEmpty();
        assertThat(result.getMetadata().getCallChains().get(0).getLinkResolution())
                .isEqualTo(LinkResolution.AMBIGUOUS_WIDEN);
    }
}
