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

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

/**
 * Expandable REST {@code {event}} entries that reach nested {@code Enum.valueOf} through an
 * abstract collaborator must produce per-constant chains / bindings so linking is not stuck on
 * {@code AMBIGUOUS_WIDEN}. No project-specific type names.
 */
class DeepDispatcherExpandableEventBindingTest {

    @Test
    void expandableEventPathShouldResolveThroughAbstractCollaboratorValueOf(@TempDir Path tempDir)
            throws Exception {
        writeFixture(tempDir);

        CodebaseContext context = new CodebaseContext();
        context.setResolveBindings(true);
        context.scan(tempDir);

        HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
        EntryPoint entry = 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();
        TriggerPoint trigger = TriggerPoint.builder()
                .className("com.example.StateMachine")
                .methodName("sendEvent")
                .event("ev")
                .build();

        List<Map<String, String>> variants =
                EntryPointBindingExpander.expandPathVariableBindings(entry, context, engine.buildCallGraph());
        assertThat(variants)
                .as("expander must reach nested abstract valueOf and emit one variant per enum constant")
                .extracting(map -> map.get("event"))
                .containsExactlyInAnyOrder("PAY", "SHIP", "CANCEL");

        List<CallChain> chains = engine.findChains(List.of(entry), List.of(trigger));
        assertThat(chains).isNotEmpty();
        assertThat(chains)
                .anySatisfy(chain -> assertThat(chain.getPathBindings()).containsEntry("event", "PAY"));
        assertThat(chains)
                .anySatisfy(chain -> assertThat(chain.getPathBindings()).containsEntry("event", "SHIP"));

        Transition pay = transition("NEW", "PAID", "PAY");
        Transition ship = transition("PAID", "SHIPPED", "SHIP");
        Transition cancel = transition("NEW", "CANCELLED", "CANCEL");

        AnalysisResult result = AnalysisResult.builder()
                .name("com.example.config.OrderStateMachineConfig")
                .eventTypeFqn("com.example.DomainEvent")
                .stateTypeFqn("com.example.DomainState")
                .transitions(List.of(pay, ship, cancel))
                .metadata(CodebaseMetadata.builder().callChains(chains).build())
                .build();
        new TriggerCanonicalizationEnricher().enrich(result, context, null);
        new TransitionLinkerEnricher().enrich(result, context, null);

        List<CallChain> linked = result.getMetadata().getCallChains();
        assertThat(linked)
                .filteredOn(c -> c.getLinkResolution() == LinkResolution.RESOLVED)
                .isNotEmpty();
        assertThat(linked)
                .filteredOn(c -> c.getLinkResolution() == LinkResolution.RESOLVED)
                .anySatisfy(c -> assertThat(c.getMatchedTransitions())
                        .anyMatch(mt -> mt.getEvent() != null && mt.getEvent().contains("PAY")));
        assertThat(linked)
                .filteredOn(c -> c.getLinkResolution() == LinkResolution.RESOLVED)
                .anySatisfy(c -> assertThat(c.getMatchedTransitions())
                        .anyMatch(mt -> mt.getEvent() != null && mt.getEvent().contains("SHIP")));
    }

    @Test
    void expandableEventPathWithFieldStashedCarrierStillResolvesViaPostHocSplit(@TempDir Path tempDir)
            throws Exception {
        // Expander cannot alias PathVariable → valueOf when the string is stashed in a field first.
        // P2 gated split must still produce one RESOLVED chain per trusted poly constant.
        writeFieldStashFixture(tempDir);

        CodebaseContext context = new CodebaseContext();
        context.setResolveBindings(true);
        context.scan(tempDir);

        HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
        EntryPoint entry = 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();
        TriggerPoint trigger = TriggerPoint.builder()
                .className("com.example.StateMachine")
                .methodName("sendEvent")
                .event("ev")
                .build();

        List<Map<String, String>> variants =
                EntryPointBindingExpander.expandPathVariableBindings(entry, context, engine.buildCallGraph());
        assertThat(variants)
                .as("field stash must defeat expander aliasing so P2 is the recovery path")
                .isEmpty();

        List<CallChain> chains = engine.findChains(List.of(entry), List.of(trigger));
        assertThat(chains).isNotEmpty();

        AnalysisResult result = AnalysisResult.builder()
                .name("com.example.config.OrderStateMachineConfig")
                .eventTypeFqn("com.example.DomainEvent")
                .stateTypeFqn("com.example.DomainState")
                .transitions(List.of(
                        transition("NEW", "PAID", "PAY"),
                        transition("PAID", "SHIPPED", "SHIP"),
                        transition("NEW", "CANCELLED", "CANCEL")))
                .metadata(CodebaseMetadata.builder().callChains(chains).build())
                .build();
        new TriggerCanonicalizationEnricher().enrich(result, context, null);
        new TransitionLinkerEnricher().enrich(result, context, null);

        List<CallChain> linked = result.getMetadata().getCallChains();
        assertThat(linked)
                .filteredOn(c -> c.getLinkResolution() == LinkResolution.RESOLVED)
                .hasSizeGreaterThanOrEqualTo(2);
        assertThat(linked)
                .filteredOn(c -> c.getLinkResolution() == LinkResolution.RESOLVED)
                .anySatisfy(c -> assertThat(c.getMatchedTransitions())
                        .anyMatch(mt -> mt.getEvent() != null && mt.getEvent().contains("PAY")));
        assertThat(linked)
                .filteredOn(c -> c.getLinkResolution() == LinkResolution.RESOLVED)
                .anySatisfy(c -> assertThat(c.getMatchedTransitions())
                        .anyMatch(mt -> mt.getEvent() != null && mt.getEvent().contains("SHIP")));
    }

    @Test
    void nonExpandableSharedEntryThroughAbstractValueOfStaysAmbiguousWiden(@TempDir Path tempDir)
            throws Exception {
        writeFixture(tempDir);

        CodebaseContext context = new CodebaseContext();
        context.setResolveBindings(true);
        context.scan(tempDir);

        HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
        // No PathVariable / no {event} placeholder — not an expandable binding entry.
        EntryPoint entry = EntryPoint.builder()
                .type(EntryPoint.Type.REST)
                .name("POST /api/machine/transition")
                .className("com.example.MachineController")
                .methodName("transition")
                .parameters(List.of(EntryPoint.Parameter.builder()
                        .name("event")
                        .type("String")
                        .annotations(List.of())
                        .build()))
                .build();
        TriggerPoint trigger = TriggerPoint.builder()
                .className("com.example.StateMachine")
                .methodName("sendEvent")
                .event("ev")
                .build();

        List<Map<String, String>> variants =
                EntryPointBindingExpander.expandPathVariableBindings(entry, context, engine.buildCallGraph());
        assertThat(variants).isEmpty();

        List<CallChain> chains = engine.findChains(List.of(entry), List.of(trigger));
        assertThat(chains).isNotEmpty();

        AnalysisResult result = AnalysisResult.builder()
                .name("com.example.config.OrderStateMachineConfig")
                .eventTypeFqn("com.example.DomainEvent")
                .stateTypeFqn("com.example.DomainState")
                .transitions(List.of(
                        transition("NEW", "PAID", "PAY"),
                        transition("PAID", "SHIPPED", "SHIP"),
                        transition("NEW", "CANCELLED", "CANCEL")))
                .metadata(CodebaseMetadata.builder().callChains(chains).build())
                .build();
        new TriggerCanonicalizationEnricher().enrich(result, context, null);
        new TransitionLinkerEnricher().enrich(result, context, null);

        assertThat(result.getMetadata().getCallChains())
                .isNotEmpty()
                .allSatisfy(c -> assertThat(c.getLinkResolution())
                        .isEqualTo(LinkResolution.AMBIGUOUS_WIDEN));
    }

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

    private static void writeFixture(Path tempDir) throws Exception {
        Files.writeString(tempDir.resolve("App.java"), """
                package com.example;

                public class MachineController {
                    private DomainCollaborator collaborator;
                    public void transition(String event) {
                        collaborator.dispatch(event);
                    }
                }

                class DomainCollaborator extends AbstractDispatcher {
                    DomainCollaborator(StateMachine sm) {
                        super(sm);
                    }
                }

                abstract class AbstractDispatcher {
                    private final StateMachine sm;
                    AbstractDispatcher(StateMachine sm) {
                        this.sm = sm;
                    }
                    void dispatch(String event) {
                        fire(event);
                    }
                    void fire(String event) {
                        DomainEvent ev = DomainEvent.valueOf(event);
                        sm.sendEvent(ev);
                    }
                }

                enum DomainEvent { PAY, SHIP, CANCEL }
                enum DomainState { NEW, PAID, SHIPPED, CANCELLED }
                class StateMachine {
                    void sendEvent(DomainEvent e) {}
                }
                """);
        writeConfig(tempDir);
    }

    private static void writeFieldStashFixture(Path tempDir) throws Exception {
        Files.writeString(tempDir.resolve("App.java"), """
                package com.example;

                public class MachineController {
                    private DomainCollaborator collaborator;
                    public void transition(String event) {
                        collaborator.stashAndDispatch(event);
                    }
                }

                class DomainCollaborator extends AbstractDispatcher {
                    DomainCollaborator(StateMachine sm) {
                        super(sm);
                    }
                }

                abstract class AbstractDispatcher {
                    private final StateMachine sm;
                    private String pendingEvent;
                    AbstractDispatcher(StateMachine sm) {
                        this.sm = sm;
                    }
                    void stashAndDispatch(String event) {
                        this.pendingEvent = event;
                        fireStashed();
                    }
                    void fireStashed() {
                        DomainEvent ev = DomainEvent.valueOf(pendingEvent);
                        sm.sendEvent(ev);
                    }
                }

                enum DomainEvent { PAY, SHIP, CANCEL }
                enum DomainState { NEW, PAID, SHIPPED, CANCELLED }
                class StateMachine {
                    void sendEvent(DomainEvent e) {}
                }
                """);
        writeConfig(tempDir);
    }

    private static void writeConfig(Path tempDir) throws Exception {
        Files.createDirectories(tempDir.resolve("com/example/config"));
        Files.writeString(tempDir.resolve("com/example/config/OrderStateMachineConfig.java"), """
                package com.example.config;
                import com.example.DomainEvent;
                import com.example.DomainState;
                import org.springframework.statemachine.config.EnumStateMachineConfigurerAdapter;
                public class OrderStateMachineConfig
                        extends EnumStateMachineConfigurerAdapter<DomainState, DomainEvent> {
                }
                """);
    }
}
