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 static org.assertj.core.api.Assertions.assertThat;

/**
 * Regression: when two enums share the same simple name across packages, we must not expand or link
 * by guessing which enum {@code OrderEvent} refers to when bindings are unavailable.
 */
class AmbiguousSimpleEnumLinkingTest {

    @Test
    void shouldFailClosedWhenEnumSimpleNameIsAmbiguousAcrossPackages(@TempDir Path tempDir) throws Exception {
        Files.createDirectories(tempDir.resolve("a"));
        Files.createDirectories(tempDir.resolve("b"));
        Files.writeString(tempDir.resolve("a/OrderEvent.java"),
                "package a; public enum OrderEvent { PAY, SHIP }");
        Files.writeString(tempDir.resolve("b/OrderEvent.java"),
                "package b; public enum OrderEvent { CANCEL }");
        Files.writeString(tempDir.resolve("App.java"), """
                package com.example;
                import a.OrderEvent;
                public class OrderController {
                    Dispatcher dispatcher = new Dispatcher();
                    public void transition(String eventStr) {
                        dispatcher.dispatch(eventStr);
                    }
                }
                class Dispatcher {
                    void dispatch(String eventStr) {
                        StateMachine sm = new StateMachine();
                        // Unresolved valueOf(arg) must not expand to a concrete enum set via simple-name lookup.
                        sm.sendEvent(OrderEvent.valueOf(eventStr));
                    }
                }
                class StateMachine { void sendEvent(OrderEvent e) {} }
                """);

        CodebaseContext context = new CodebaseContext();
        // Keep bindings off so type printing stays import-style and we rely on scan-time indexes.
        context.setResolveBindings(false);
        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 pay = new Transition();
        pay.setSourceStates(List.of(State.of("NEW", "NEW")));
        pay.setTargetStates(List.of(State.of("PAID", "PAID")));
        pay.setEvent(Event.of("PAY", "a.OrderEvent.PAY"));

        Transition ship = new Transition();
        ship.setSourceStates(List.of(State.of("PAID", "PAID")));
        ship.setTargetStates(List.of(State.of("SHIPPED", "SHIPPED")));
        ship.setEvent(Event.of("SHIP", "a.OrderEvent.SHIP"));

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

        new TransitionLinkerEnricher().enrich(result, context, null);

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

    @Test
    void shouldFailClosedWhenEnumExpansionHintUsesAmbiguousSimpleName(@TempDir Path tempDir) throws Exception {
        Files.createDirectories(tempDir.resolve("a"));
        Files.createDirectories(tempDir.resolve("b"));
        Files.writeString(tempDir.resolve("a/OrderEvent.java"),
                "package a; public enum OrderEvent { PAY, SHIP }");
        Files.writeString(tempDir.resolve("b/OrderEvent.java"),
                "package b; public enum OrderEvent { CANCEL }");
        Files.writeString(tempDir.resolve("App.java"), """
                package com.example;
                import a.OrderEvent;
                public class OrderController {
                    Dispatcher dispatcher = new Dispatcher();
                    public void transition(String eventStr) {
                        dispatcher.dispatch(eventStr);
                    }
                }
                class Dispatcher {
                    void dispatch(String eventStr) {
                        StateMachine sm = new StateMachine();
                        sm.sendEvent(OrderEvent.valueOf(normalize(eventStr)));
                    }
                    String normalize(String value) {
                        return value.trim().toUpperCase();
                    }
                }
                class StateMachine { void sendEvent(OrderEvent e) {} }
                """);

        CodebaseContext context = new CodebaseContext();
        context.setResolveBindings(false);
        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 pay = new Transition();
        pay.setSourceStates(List.of(State.of("NEW", "NEW")));
        pay.setTargetStates(List.of(State.of("PAID", "PAID")));
        pay.setEvent(Event.of("PAY", "a.OrderEvent.PAY"));

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

        new TransitionLinkerEnricher().enrich(result, context, null);

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

    @Test
    void shouldFailClosedWhenMachineEventTypeWouldExpandAmbiguousSymbolic(@TempDir Path tempDir) throws Exception {
        Files.createDirectories(tempDir.resolve("a"));
        Files.createDirectories(tempDir.resolve("b"));
        Files.writeString(tempDir.resolve("a/OrderEvent.java"),
                "package a; public enum OrderEvent { PAY, SHIP }");
        Files.writeString(tempDir.resolve("b/OrderEvent.java"),
                "package b; public enum OrderEvent { CANCEL }");
        Files.writeString(tempDir.resolve("App.java"), """
                package com.example;
                import a.OrderEvent;
                public class OrderController {
                    Dispatcher dispatcher = new Dispatcher();
                    public void transition(String eventStr) {
                        dispatcher.dispatch(eventStr);
                    }
                }
                class Dispatcher {
                    void dispatch(String eventStr) {
                        StateMachine sm = new StateMachine();
                        sm.sendEvent(OrderEvent.valueOf(eventStr));
                    }
                }
                class StateMachine { void sendEvent(OrderEvent e) {} }
                """);

        CodebaseContext context = new CodebaseContext();
        context.setResolveBindings(false);
        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 pay = new Transition();
        pay.setSourceStates(List.of(State.of("NEW", "NEW")));
        pay.setTargetStates(List.of(State.of("PAID", "PAID")));
        pay.setEvent(Event.of("PAY", "a.OrderEvent.PAY"));

        Transition ship = new Transition();
        ship.setSourceStates(List.of(State.of("PAID", "PAID")));
        ship.setTargetStates(List.of(State.of("SHIPPED", "SHIPPED")));
        ship.setEvent(Event.of("SHIP", "a.OrderEvent.SHIP"));

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

        new TransitionLinkerEnricher().enrich(result, context, null);

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

    @Test
    void shouldCanonicalizeImportStylePolyWhenMachineEventTypeIsKnown(@TempDir Path tempDir) throws Exception {
        Files.createDirectories(tempDir.resolve("a"));
        Files.createDirectories(tempDir.resolve("b"));
        Files.writeString(tempDir.resolve("a/OrderEvent.java"),
                "package a; public enum OrderEvent { PAY }");
        Files.writeString(tempDir.resolve("b/OrderEvent.java"),
                "package b; public enum OrderEvent { CANCEL }");

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

        TriggerPoint trigger = TriggerPoint.builder()
                .event("OrderEvent.valueOf(eventStr)")
                .polymorphicEvents(List.of("OrderEvent.PAY", "OrderEvent.SHIP"))
                .ambiguous(true)
                .build();

        AnalysisResult result = AnalysisResult.builder()
                .name("com.example.OrderStateMachineConfig")
                .eventTypeFqn("a.OrderEvent")
                .transitions(List.of())
                .metadata(CodebaseMetadata.builder()
                        .callChains(List.of(CallChain.builder().triggerPoint(trigger).build()))
                        .build())
                .build();

        new TriggerCanonicalizationEnricher().enrich(result, context, null);

        TriggerPoint canonical = result.getMetadata().getCallChains().get(0).getTriggerPoint();
        assertThat(canonical.getPolymorphicEvents())
                .containsExactly("a.OrderEvent.PAY", "a.OrderEvent.SHIP");
    }
}

