package click.kamil.springstatemachineexporter.analysis.enricher;

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.AnalysisResult;
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 java.util.Map;

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

/**
 * P2: expandable event-carrier entries with unbound {@code valueOf} multi-poly must split into
 * one chain per constant; non-expandable shared entries must stay fail-closed.
 */
class ExpandableEntryPolySplitterTest {

    private final TransitionLinkerEnricher enricher = new TransitionLinkerEnricher();

    @Test
    void maybeExpandSplitsTrustedMultiPolyForExpandableEventCarrier() {
        CallChain chain = expandableCarrierChain(null);
        TriggerPoint prepared = chain.getTriggerPoint();

        List<CallChain> split = ExpandableEntryPolySplitter.maybeExpand(
                chain, prepared, "com.example.OrderEvent", null);

        assertThat(split).hasSize(2);
        assertThat(split)
                .extracting(c -> c.getTriggerPoint().getPolymorphicEvents())
                .containsExactlyInAnyOrder(
                        List.of("com.example.OrderEvent.PAY"),
                        List.of("com.example.OrderEvent.SHIP"));
        assertThat(split)
                .allSatisfy(c -> {
                    assertThat(c.getTriggerPoint().isAmbiguous()).isFalse();
                    assertThat(c.getTriggerPoint().isExternal()).isFalse();
                    assertThat(c.getPathBindings()).containsKey("event");
                });
    }

    @Test
    void maybeExpandDoesNotSplitWhenCarrierAlreadyBound() {
        CallChain chain = expandableCarrierChain(Map.of("event", "PAY"));
        List<CallChain> out = ExpandableEntryPolySplitter.maybeExpand(
                chain, chain.getTriggerPoint(), "com.example.OrderEvent", null);
        assertThat(out).hasSize(1);
        assertThat(out.get(0).getTriggerPoint().getPolymorphicEvents()).hasSize(2);
    }

    @Test
    void maybeExpandDoesNotSplitWhenMachineDiscriminatorUnbound() {
        CallChain chain = CallChain.builder()
                .entryPoint(EntryPoint.builder()
                        .type(EntryPoint.Type.REST)
                        .name("POST /api/machine/{machineType}/transition/{event}")
                        .className("com.example.MachineController")
                        .methodName("transition")
                        .parameters(List.of(
                                EntryPoint.Parameter.builder()
                                        .name("machineType")
                                        .type("String")
                                        .annotations(List.of("PathVariable"))
                                        .build(),
                                EntryPoint.Parameter.builder()
                                        .name("event")
                                        .type("String")
                                        .annotations(List.of("PathVariable"))
                                        .build()))
                        .build())
                .triggerPoint(multiValueOfTrigger())
                .build();

        assertThat(ExpandableEntryPolySplitter.shouldSplit(
                chain, chain.getTriggerPoint(), "com.example.OrderEvent", null)).isFalse();
        assertThat(ExpandableEntryPolySplitter.maybeExpand(
                chain, chain.getTriggerPoint(), "com.example.OrderEvent", null)).hasSize(1);
    }

    @Test
    void maybeExpandSplitsWhenMachineDiscriminatorAlreadyBound() {
        CallChain chain = CallChain.builder()
                .entryPoint(EntryPoint.builder()
                        .type(EntryPoint.Type.REST)
                        .name("POST /api/machine/{machineType}/transition/{event}")
                        .className("com.example.MachineController")
                        .methodName("transition")
                        .build())
                .pathBindings(Map.of("machineType", "ORDER"))
                .triggerPoint(multiValueOfTrigger())
                .build();

        List<CallChain> split = ExpandableEntryPolySplitter.maybeExpand(
                chain, chain.getTriggerPoint(), "com.example.OrderEvent", null);
        assertThat(split).hasSize(2);
    }

    @Test
    void maybeExpandDoesNotSplitNonExpandableSharedEntry() {
        CallChain chain = CallChain.builder()
                .entryPoint(EntryPoint.builder()
                        .type(EntryPoint.Type.REST)
                        .name("POST /api/order/adjust")
                        .className("com.example.OrderController")
                        .methodName("adjust")
                        .build())
                .triggerPoint(multiValueOfTrigger())
                .build();

        List<CallChain> out = ExpandableEntryPolySplitter.maybeExpand(
                chain, chain.getTriggerPoint(), "com.example.OrderEvent", null);
        assertThat(out).hasSize(1);
        assertThat(ExpandableEntryPolySplitter.shouldSplit(
                chain, chain.getTriggerPoint(), "com.example.OrderEvent", null)).isFalse();
    }

    @Test
    void linkerSplitsExpandableUnboundValueOfIntoResolvedChains() {
        Transition pay = transition("NEW", "PAID", "PAY");
        Transition ship = transition("PAID", "SHIPPED", "SHIP");

        CallChain chain = expandableCarrierChain(null).toBuilder()
                .triggerPoint(multiValueOfTrigger().toBuilder().external(true).build())
                .build();

        AnalysisResult result = AnalysisResult.builder()
                .name("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 -> mt.getEvent() != null && mt.getEvent().contains("PAY")));
        assertThat(linked)
                .anySatisfy(c -> assertThat(c.getMatchedTransitions())
                        .anyMatch(mt -> mt.getEvent() != null && mt.getEvent().contains("SHIP")));
    }

    @Test
    void linkerKeepsAmbiguousWidenForNonExpandableSharedValueOf() {
        Transition pay = transition("NEW", "PAID", "PAY");
        Transition ship = transition("PAID", "SHIPPED", "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(multiValueOfTrigger())
                .build();

        AnalysisResult result = AnalysisResult.builder()
                .name("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);

        assertThat(result.getMetadata().getCallChains())
                .hasSize(1)
                .allSatisfy(c -> {
                    assertThat(c.getLinkResolution()).isEqualTo(LinkResolution.AMBIGUOUS_WIDEN);
                    assertThat(c.getMatchedTransitions()).isNullOrEmpty();
                });
    }

    private static CallChain expandableCarrierChain(Map<String, String> pathBindings) {
        return CallChain.builder()
                .entryPoint(EntryPoint.builder()
                        .type(EntryPoint.Type.REST)
                        .name("POST /api/machine/transition/{event}")
                        .className("com.example.MachineController")
                        .methodName("transition")
                        .parameters(List.of(EntryPoint.Parameter.builder()
                                .name("event")
                                .type("String")
                                .annotations(List.of("PathVariable"))
                                .build()))
                        .build())
                .pathBindings(pathBindings)
                .triggerPoint(multiValueOfTrigger())
                .build();
    }

    private static TriggerPoint multiValueOfTrigger() {
        return TriggerPoint.builder()
                .ambiguous(true)
                .eventTypeFqn("com.example.OrderEvent")
                .event("OrderEvent.valueOf(eventStr)")
                .polymorphicEvents(List.of(
                        "com.example.OrderEvent.PAY",
                        "com.example.OrderEvent.SHIP"))
                .build();
    }

    private static Transition transition(String from, String to, String event) {
        Transition t = new Transition();
        t.setSourceStates(List.of(State.of(from, "com.example.OrderState." + from)));
        t.setTargetStates(List.of(State.of(to, "com.example.OrderState." + to)));
        t.setEvent(Event.of(event, "com.example.OrderEvent." + event));
        return t;
    }
}
