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.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 CallChainPolyNarrowerTest {

    private final TransitionLinkerEnricher enricher = new TransitionLinkerEnricher();

    @Test
    void shouldNotNarrowUsingRestPathSegmentAlone() {
        Transition pay = transition("NEW", "PAID", "com.example.OrderEvent.PAY");
        Transition ship = transition("PAID", "SHIPPED", "com.example.OrderEvent.SHIP");

        // Wide poly + URL containing /PAY must NOT invent a narrow — that is not dataflow.
        CallChain chain = CallChain.builder()
                .entryPoint(EntryPoint.builder()
                        .name("POST /api/machine/ORDER/transition/PAY")
                        .className("com.example.MachineController")
                        .methodName("transition")
                        .build())
                .methodChain(List.of(
                        "com.example.MachineController.transition",
                        "com.example.StateMachineDispatcher.fireOrder",
                        "com.example.StateMachine.send"))
                .triggerPoint(TriggerPoint.builder()
                        .ambiguous(true)
                        .event("OrderEvent.valueOf(eventStr)")
                        .polymorphicEvents(List.of(
                                "com.example.OrderEvent.PAY",
                                "com.example.OrderEvent.SHIP",
                                "com.example.OrderEvent.CANCEL"))
                        .build())
                .build();

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

        enricher.enrich(result, null, null);

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

    @Test
    void shouldNarrowUsingConstraintBindingEvidence() {
        Transition pay = transition("NEW", "PAID", "com.example.OrderEvent.PAY");
        Transition ship = transition("PAID", "SHIPPED", "com.example.OrderEvent.SHIP");

        CallChain chain = CallChain.builder()
                .entryPoint(EntryPoint.builder()
                        .name("POST /api/machine/ORDER/transition/PAY")
                        .className("com.example.MachineController")
                        .methodName("transition")
                        .build())
                .methodChain(List.of(
                        "com.example.MachineController.transition",
                        "com.example.StateMachineDispatcher.fireOrder",
                        "com.example.StateMachine.send"))
                .triggerPoint(TriggerPoint.builder()
                        .ambiguous(true)
                        .event("OrderEvent.valueOf(eventStr)")
                        .constraint("\"PAY\".equalsIgnoreCase(event)")
                        .polymorphicEvents(List.of(
                                "com.example.OrderEvent.PAY",
                                "com.example.OrderEvent.SHIP",
                                "com.example.OrderEvent.CANCEL"))
                        .build())
                .build();

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

        enricher.enrich(result, null, null);

        CallChain linked = result.getMetadata().getCallChains().get(0);
        assertThat(linked.getLinkResolution()).isEqualTo(LinkResolution.RESOLVED);
        assertThat(linked.getMatchedTransitions()).hasSize(1);
        assertThat(linked.getMatchedTransitions().get(0).getEvent())
                .isEqualTo("com.example.OrderEvent.PAY");
        assertThat(linked.getTriggerPoint().getPolymorphicEvents())
                .containsExactly("com.example.OrderEvent.PAY");
    }

    @Test
    void shouldNotNarrowUsingMethodNameTokensAlone() {
        Transition pay = transition("NEW", "PAID", "com.example.OrderEvent.PAY");
        Transition ship = transition("PAID", "SHIPPED", "com.example.OrderEvent.SHIP");

        CallChain chain = CallChain.builder()
                .entryPoint(EntryPoint.builder()
                        .name("POST /api/orders/rich/pay")
                        .className("com.example.RichOrderController")
                        .methodName("payViaRichEvent")
                        .build())
                .methodChain(List.of(
                        "com.example.RichOrderController.payViaRichEvent",
                        "com.example.CommandGateway.payOrderViaRichEvent",
                        "com.example.CentralEventDispatcher.orderPayViaRichEvent",
                        "com.example.CentralEventDispatcher.sendOrderEvent"))
                .triggerPoint(TriggerPoint.builder()
                        .ambiguous(true)
                        .event("richEvent.getType()")
                        .polymorphicEvents(List.of(
                                "com.example.OrderEvent.PAY",
                                "com.example.OrderEvent.SHIP",
                                "com.example.OrderEvent.CANCEL"))
                        .build())
                .build();

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

        enricher.enrich(result, null, null);

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

    @Test
    void shouldNarrowUsingPathBindingsFromDataflow() {
        Transition pay = transition("NEW", "PAID", "com.example.OrderEvent.PAY");
        Transition ship = transition("PAID", "SHIPPED", "com.example.OrderEvent.SHIP");

        CallChain chain = CallChain.builder()
                .entryPoint(EntryPoint.builder()
                        .name("POST /api/orders/pay")
                        .className("com.example.OrderController")
                        .methodName("pay")
                        .build())
                .pathBindings(java.util.Map.of("event", "com.example.OrderEvent.PAY"))
                .triggerPoint(TriggerPoint.builder()
                        .ambiguous(true)
                        .event("OrderEvent.valueOf(eventStr)")
                        .polymorphicEvents(List.of(
                                "com.example.OrderEvent.PAY",
                                "com.example.OrderEvent.SHIP",
                                "com.example.OrderEvent.CANCEL"))
                        .build())
                .build();

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

        enricher.enrich(result, null, null);

        CallChain linked = result.getMetadata().getCallChains().get(0);
        assertThat(linked.getLinkResolution()).isEqualTo(LinkResolution.RESOLVED);
        assertThat(linked.getMatchedTransitions()).hasSize(1);
        assertThat(linked.getMatchedTransitions().get(0).getEvent())
                .isEqualTo("com.example.OrderEvent.PAY");
    }

    @Test
    void shouldNarrowUsingDottedCommandKeyPath() {
        Transition pay = transition("NEW", "PAID", "com.example.OrderEvent.PAY");
        Transition ship = transition("PAID", "SHIPPED", "com.example.OrderEvent.SHIP");

        CallChain chain = CallChain.builder()
                .entryPoint(EntryPoint.builder()
                        .name("POST /api/commands/order.pay")
                        .className("com.example.GenericCommandController")
                        .methodName("execute")
                        .build())
                .methodChain(List.of(
                        "com.example.GenericCommandController.execute",
                        "com.example.CommandGateway.executeViaMapper",
                        "com.example.CentralEventDispatcher.orderPay",
                        "com.example.CentralEventDispatcher.sendOrderEvent"))
                .triggerPoint(TriggerPoint.builder()
                        .ambiguous(true)
                        .event("OrderEvent.valueOf(actionKey)")
                        .constraint("\"order.pay\".equalsIgnoreCase(commandKey) && command == ORDER_PAY")
                        .polymorphicEvents(List.of(
                                "com.example.OrderEvent.PAY",
                                "com.example.OrderEvent.SHIP",
                                "com.example.OrderEvent.CANCEL"))
                        .build())
                .build();

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

        enricher.enrich(result, null, null);

        CallChain linked = result.getMetadata().getCallChains().get(0);
        assertThat(linked.getLinkResolution()).isEqualTo(LinkResolution.RESOLVED);
        assertThat(linked.getMatchedTransitions()).hasSize(1);
        assertThat(linked.getMatchedTransitions().get(0).getEvent())
                .isEqualTo("com.example.OrderEvent.PAY");
    }

    @Test
    void shouldNarrowWidePolyUsingSendEventLiteralInCallerMethod(@TempDir Path tempDir) throws Exception {
        String source = """
            package com.example;
            public class OrderController {
                CentralEventDispatcher dispatcher;
                public void pay() { dispatcher.orderPay(); }
            }
            class CentralEventDispatcher {
                void orderPay() { sendOrderEvent(OrderEvent.PAY); }
                void orderShip() { sendOrderEvent(OrderEvent.SHIP); }
                private void sendOrderEvent(OrderEvent event) {
                    StateMachine sm = new StateMachine();
                    sm.sendEvent(event);
                }
            }
            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);

        Transition pay = transition("NEW", "PAID", "com.example.OrderEvent.PAY");
        Transition ship = transition("PAID", "SHIPPED", "com.example.OrderEvent.SHIP");

        CallChain chain = CallChain.builder()
                .entryPoint(EntryPoint.builder()
                        .name("POST /api/orders/pay")
                        .className("com.example.OrderController")
                        .methodName("pay")
                        .build())
                .methodChain(List.of(
                        "com.example.OrderController.pay",
                        "com.example.CentralEventDispatcher.orderPay",
                        "com.example.CentralEventDispatcher.sendOrderEvent",
                        "com.example.StateMachine.sendEvent"))
                .triggerPoint(TriggerPoint.builder()
                        .ambiguous(true)
                        .event("event")
                        .polymorphicEvents(List.of(
                                "com.example.OrderEvent.PAY",
                                "com.example.OrderEvent.SHIP",
                                "com.example.OrderEvent.CANCEL"))
                        .build())
                .build();

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

        enricher.enrich(result, context, null);

        CallChain linked = result.getMetadata().getCallChains().get(0);
        assertThat(linked.getLinkResolution()).isEqualTo(LinkResolution.RESOLVED);
        assertThat(linked.getMatchedTransitions()).hasSize(1);
        assertThat(linked.getMatchedTransitions().get(0).getEvent())
                .isEqualTo("com.example.OrderEvent.PAY");
    }

    @Test
    void shouldNarrowWidePolyUsingRichEventParameterType(@TempDir Path tempDir) throws Exception {
        String source = """
            package com.example;
            public class RichOrderController {
                CentralEventDispatcher dispatcher;
                public void payViaRichEvent() {
                    dispatcher.orderPayViaRichEvent(new PayRichOrderEvent());
                }
            }
            class PayRichOrderEvent {
                OrderEvent getType() { return OrderEvent.PAY; }
            }
            class CentralEventDispatcher {
                void orderPayViaRichEvent(PayRichOrderEvent event) {
                    sendOrderEvent(event.getType());
                }
                private void sendOrderEvent(OrderEvent event) {
                    StateMachine sm = new StateMachine();
                    sm.sendEvent(event);
                }
            }
            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);

        Transition pay = transition("NEW", "PAID", "com.example.OrderEvent.PAY");
        Transition ship = transition("PAID", "SHIPPED", "com.example.OrderEvent.SHIP");

        CallChain chain = CallChain.builder()
                .entryPoint(EntryPoint.builder()
                        .name("POST /api/orders/rich/pay")
                        .className("com.example.RichOrderController")
                        .methodName("payViaRichEvent")
                        .build())
                .methodChain(List.of(
                        "com.example.RichOrderController.payViaRichEvent",
                        "com.example.CentralEventDispatcher.orderPayViaRichEvent",
                        "com.example.CentralEventDispatcher.sendOrderEvent",
                        "com.example.StateMachine.sendEvent"))
                .triggerPoint(TriggerPoint.builder()
                        .ambiguous(true)
                        .event("event.getType()")
                        .polymorphicEvents(List.of(
                                "com.example.OrderEvent.PAY",
                                "com.example.OrderEvent.SHIP",
                                "com.example.OrderEvent.CANCEL"))
                        .build())
                .build();

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

        enricher.enrich(result, context, null);

        CallChain linked = result.getMetadata().getCallChains().get(0);
        assertThat(linked.getLinkResolution()).isEqualTo(LinkResolution.RESOLVED);
        assertThat(linked.getMatchedTransitions()).hasSize(1);
        assertThat(linked.getMatchedTransitions().get(0).getEvent())
                .isEqualTo("com.example.OrderEvent.PAY");
    }

    @Test
    void shouldNarrowWidePolyWhenConfigEventTypeFqnIsMissing() {
        Transition pay = transition("NEW", "PAID", "com.example.OrderEvent.PAY");
        Transition ship = transition("PAID", "SHIPPED", "com.example.OrderEvent.SHIP");

        // Without eventTypeFqn, path text must not invent PAY — require dataflow bindings/constraints.
        CallChain chain = CallChain.builder()
                .entryPoint(EntryPoint.builder()
                        .name("POST /api/machine/ORDER/transition/PAY")
                        .className("com.example.MachineController")
                        .methodName("transition")
                        .build())
                .pathBindings(java.util.Map.of("event", "PAY"))
                .triggerPoint(TriggerPoint.builder()
                        .ambiguous(true)
                        .event("OrderEvent.valueOf(eventStr)")
                        .eventTypeFqn("com.example.OrderEvent")
                        .polymorphicEvents(List.of(
                                "com.example.OrderEvent.PAY",
                                "com.example.OrderEvent.SHIP",
                                "com.example.OrderEvent.CANCEL"))
                        .build())
                .build();

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

        enricher.enrich(result, null, null);

        CallChain linked = result.getMetadata().getCallChains().get(0);
        assertThat(linked.getLinkResolution()).isEqualTo(LinkResolution.RESOLVED);
        assertThat(linked.getMatchedTransitions()).hasSize(1);
        assertThat(linked.getMatchedTransitions().get(0).getEvent())
                .isEqualTo("com.example.OrderEvent.PAY");
    }

    @Test
    void expandableUnboundEventOnlyTemplateSplitsTrustedValueOfWiden() {
        // P2: expandable {event} alone + unbound trusted Enum.valueOf multi-poly → split.
        Transition pay = transition("NEW", "PAID", "com.example.OrderEvent.PAY");
        Transition ship = transition("PAID", "SHIPPED", "com.example.OrderEvent.SHIP");

        CallChain chain = CallChain.builder()
                .entryPoint(EntryPoint.builder()
                        .name("POST /api/machine/transition/{event}")
                        .className("com.example.MachineController")
                        .methodName("transition")
                        .build())
                .triggerPoint(TriggerPoint.builder()
                        .ambiguous(true)
                        .external(true)
                        .event("OrderEvent.valueOf(eventStr)")
                        .polymorphicEvents(List.of(
                                "com.example.OrderEvent.PAY",
                                "com.example.OrderEvent.SHIP"))
                        .build())
                .build();

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

        enricher.enrich(result, null, null);

        List<CallChain> linked = result.getMetadata().getCallChains();
        assertThat(linked).hasSize(2);
        assertThat(linked)
                .allSatisfy(c -> assertThat(c.getLinkResolution()).isEqualTo(LinkResolution.RESOLVED));
        assertThat(linked)
                .anySatisfy(c -> assertThat(c.getMatchedTransitions())
                        .anyMatch(mt -> "com.example.OrderEvent.PAY".equals(mt.getEvent())));
        assertThat(linked)
                .anySatisfy(c -> assertThat(c.getMatchedTransitions())
                        .anyMatch(mt -> "com.example.OrderEvent.SHIP".equals(mt.getEvent())));
    }

    @Test
    void dualUnboundMachineAndEventTemplateStaysFailClosed() {
        // P2 must not fan out events while {machineType} is still unbound.
        Transition pay = transition("NEW", "PAID", "com.example.OrderEvent.PAY");
        Transition ship = transition("PAID", "SHIPPED", "com.example.OrderEvent.SHIP");

        CallChain chain = CallChain.builder()
                .entryPoint(EntryPoint.builder()
                        .name("POST /api/machine/{machineType}/transition/{event}")
                        .className("com.example.MachineController")
                        .methodName("transition")
                        .build())
                .triggerPoint(TriggerPoint.builder()
                        .ambiguous(true)
                        .external(true)
                        .event("OrderEvent.valueOf(eventStr)")
                        .polymorphicEvents(List.of(
                                "com.example.OrderEvent.PAY",
                                "com.example.OrderEvent.SHIP"))
                        .build())
                .build();

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

        enricher.enrich(result, null, null);

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

    @Test
    void dualTemplateWithBoundMachineTypeStillSplitsEventWiden() {
        Transition pay = transition("NEW", "PAID", "com.example.OrderEvent.PAY");
        Transition ship = transition("PAID", "SHIPPED", "com.example.OrderEvent.SHIP");

        CallChain chain = CallChain.builder()
                .entryPoint(EntryPoint.builder()
                        .name("POST /api/machine/{machineType}/transition/{event}")
                        .className("com.example.MachineController")
                        .methodName("transition")
                        .build())
                .pathBindings(java.util.Map.of("machineType", "ORDER"))
                .triggerPoint(TriggerPoint.builder()
                        .ambiguous(true)
                        .external(true)
                        .event("OrderEvent.valueOf(eventStr)")
                        .polymorphicEvents(List.of(
                                "com.example.OrderEvent.PAY",
                                "com.example.OrderEvent.SHIP"))
                        .build())
                .build();

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

        enricher.enrich(result, null, null);

        List<CallChain> linked = result.getMetadata().getCallChains();
        assertThat(linked).hasSize(2);
        assertThat(linked)
                .allSatisfy(c -> assertThat(c.getLinkResolution()).isEqualTo(LinkResolution.RESOLVED));
    }

    @Test
    void nonExpandableWidePolyWithoutEventCarrierStaysFailClosed() {
        Transition pay = transition("NEW", "PAID", "com.example.OrderEvent.PAY");
        Transition ship = transition("PAID", "SHIPPED", "com.example.OrderEvent.SHIP");

        CallChain chain = CallChain.builder()
                .entryPoint(EntryPoint.builder()
                        .name("POST /api/machine/order/adjust")
                        .className("com.example.MachineController")
                        .methodName("adjust")
                        .build())
                .triggerPoint(TriggerPoint.builder()
                        .ambiguous(true)
                        .event("OrderEvent.valueOf(eventStr)")
                        .polymorphicEvents(List.of(
                                "com.example.OrderEvent.PAY",
                                "com.example.OrderEvent.SHIP"))
                        .build())
                .build();

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

        enricher.enrich(result, null, null);

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

    private static Transition transition(String source, String target, String eventFqn) {
        Transition transition = new Transition();
        transition.setSourceStates(List.of(State.of(source, "com.example.OrderState." + source)));
        transition.setTargetStates(List.of(State.of(target, "com.example.OrderState." + target)));
        transition.setEvent(Event.of(eventFqn.substring(eventFqn.lastIndexOf('.') + 1), eventFqn));
        return transition;
    }
}
