package click.kamil.springstatemachineexporter.analysis.service;

import click.kamil.springstatemachineexporter.analysis.enricher.TransitionLinkerEnricher;
import click.kamil.springstatemachineexporter.analysis.enricher.routing.HeuristicBeanResolutionEngine;
import click.kamil.springstatemachineexporter.analysis.enricher.routing.SharedServiceRoutingPolicy;
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;

/**
 * End-to-end: {@code SharedService&lt;S,E&gt;} sendEvent sites start unbound; after call-chain
 * resolution the subclass parameterization rebinds concrete state/event FQNs so routing uses
 * type affinity instead of shared-infra fallback.
 */
class SharedGenericBaseTypeRebindingTest {

    private final HeuristicBeanResolutionEngine routingEngine = new HeuristicBeanResolutionEngine();

    @Test
    void orderAndDocumentChainsRebindConcreteTypesAndStayIsolated(@TempDir Path tempDir) throws Exception {
        writeProject(tempDir);

        CodebaseContext context = scan(tempDir);
        List<TriggerPoint> triggers = detectTriggers(context);
        assertThat(triggers).isNotEmpty();

        HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);

        List<CallChain> orderChains = engine.findChains(
                List.of(EntryPoint.builder()
                        .type(EntryPoint.Type.REST)
                        .name("POST /orders/pay")
                        .className("com.example.OrderController")
                        .methodName("pay")
                        .build()),
                triggers);

        assertThat(orderChains).isNotEmpty();
        TriggerPoint orderTrigger = orderChains.get(0).getTriggerPoint();
        assertThat(orderTrigger.getEventTypeFqn())
                .as("OrderService extends SharedService<OrderState, OrderEvent> must rebind E")
                .isEqualTo("com.example.OrderEvent");
        assertThat(orderTrigger.getStateTypeFqn())
                .as("OrderService extends SharedService<OrderState, OrderEvent> must rebind S")
                .isEqualTo("com.example.OrderState");
        assertThat(SharedServiceRoutingPolicy.isProvablySharedInfrastructure(orderChains.get(0)))
                .as("concrete S/E must demote shared-infra")
                .isFalse();

        List<CallChain> documentChains = engine.findChains(
                List.of(EntryPoint.builder()
                        .type(EntryPoint.Type.REST)
                        .name("POST /documents/submit")
                        .className("com.example.DocumentController")
                        .methodName("submit")
                        .build()),
                triggers);

        assertThat(documentChains).isNotEmpty();
        TriggerPoint documentTrigger = documentChains.get(0).getTriggerPoint();
        assertThat(documentTrigger.getEventTypeFqn()).isEqualTo("com.example.DocumentEvent");
        assertThat(documentTrigger.getStateTypeFqn()).isEqualTo("com.example.DocumentState");
        assertThat(SharedServiceRoutingPolicy.isProvablySharedInfrastructure(documentChains.get(0)))
                .isFalse();

        Transition pay = transition("PAY", "com.example.OrderEvent.PAY", "NEW", "PAID");
        Transition submit = transition("SUBMIT", "com.example.DocumentEvent.SUBMIT", "DRAFT", "SUBMITTED");

        assertThat(routingEngine.hasProvenMachineAffinity(
                orderChains.get(0), "com.example.OrderStateMachineConfig", context, List.of(pay)))
                .isTrue();
        assertThat(routingEngine.hasProvenMachineAffinity(
                orderChains.get(0), "com.example.DocumentStateMachineConfig", context, List.of(submit)))
                .isFalse();
        assertThat(routingEngine.hasProvenMachineAffinity(
                documentChains.get(0), "com.example.DocumentStateMachineConfig", context, List.of(submit)))
                .isTrue();
        assertThat(routingEngine.hasProvenMachineAffinity(
                documentChains.get(0), "com.example.OrderStateMachineConfig", context, List.of(pay)))
                .isFalse();

        AnalysisResult orderResult = AnalysisResult.builder()
                .name("com.example.OrderStateMachineConfig")
                .eventTypeFqn("com.example.OrderEvent")
                .stateTypeFqn("com.example.OrderState")
                .transitions(List.of(pay))
                .metadata(CodebaseMetadata.builder().callChains(orderChains).build())
                .build();
        new TransitionLinkerEnricher().enrich(orderResult, context, null);
        CallChain linkedOrder = orderResult.getMetadata().getCallChains().get(0);
        assertThat(linkedOrder.getLinkResolution()).isEqualTo(LinkResolution.RESOLVED);
        assertThat(linkedOrder.getMatchedTransitions()).hasSize(1);
        assertThat(linkedOrder.getMatchedTransitions().get(0).getEvent())
                .contains("PAY");

        AnalysisResult documentResult = AnalysisResult.builder()
                .name("com.example.DocumentStateMachineConfig")
                .eventTypeFqn("com.example.DocumentEvent")
                .stateTypeFqn("com.example.DocumentState")
                .transitions(List.of(submit))
                .metadata(CodebaseMetadata.builder().callChains(documentChains).build())
                .build();
        new TransitionLinkerEnricher().enrich(documentResult, context, null);
        CallChain linkedDocument = documentResult.getMetadata().getCallChains().get(0);
        assertThat(linkedDocument.getLinkResolution()).isEqualTo(LinkResolution.RESOLVED);
        assertThat(linkedDocument.getMatchedTransitions()).hasSize(1);
        assertThat(linkedDocument.getMatchedTransitions().get(0).getEvent())
                .contains("SUBMIT");
    }

    @Test
    void stillUnboundWithoutSubclassOnPathKeepsSharedInfraFallback(@TempDir Path tempDir) throws Exception {
        writeProject(tempDir);

        CodebaseContext context = scan(tempDir);

        // No OrderService/DocumentService on the chain — rebinding cannot prove a unique S/E.
        CallChain unboundChain = CallChain.builder()
                .triggerPoint(TriggerPoint.builder()
                        .className("com.example.SharedService")
                        .methodName("updateState")
                        .event("com.example.OrderEvent.PAY")
                        .polymorphicEvents(List.of("com.example.OrderEvent.PAY"))
                        .eventTypeFqn("E")
                        .stateTypeFqn("S")
                        .build())
                .methodChain(List.of(
                        "com.example.CommonController.dispatch()",
                        "com.example.SharedService.updateState()"))
                .build();

        TriggerPoint rebound = TriggerMachineTypeRebinder.rebind(
                unboundChain.getTriggerPoint(), unboundChain.getMethodChain(), context);

        // Poly-owner may recover event type; state stays unbound without subclass args.
        assertThat(rebound.getStateTypeFqn()).isNull();
        CallChain afterRebind = unboundChain.toBuilder().triggerPoint(rebound).build();

        Transition pay = transition("PAY", "com.example.OrderEvent.PAY", "NEW", "PAID");
        // With only event type rebound (or still absent types + poly), affinity to order machine
        // remains via poly / shared-infra — not via invented Document types.
        assertThat(routingEngine.hasProvenMachineAffinity(
                afterRebind, "com.example.OrderStateMachineConfig", context, List.of(pay)))
                .isTrue();
    }

    private static List<TriggerPoint> detectTriggers(CodebaseContext context) {
        GenericEventDetector detector = new GenericEventDetector(
                context, context.getConstantResolver(), List.of());
        return context.getCompilationUnits().stream()
                .flatMap(cu -> detector.detect(cu).stream())
                .filter(tp -> "updateState".equals(tp.getMethodName())
                        || "sendEvent".equals(tp.getMethodName()))
                .toList();
    }

    private static CodebaseContext scan(Path tempDir) throws Exception {
        CodebaseContext context = new CodebaseContext();
        context.setResolveBindings(true);
        context.setSourcepath(List.of(tempDir.resolve("src/main/java").toString()));
        context.scan(tempDir);
        return context;
    }

    private static void writeProject(Path tempDir) throws Exception {
        Path javaRoot = tempDir.resolve("src/main/java/com/example");
        Files.createDirectories(javaRoot);
        Files.writeString(tempDir.resolve("build.gradle"), "plugins { id 'java' }");

        Files.writeString(javaRoot.resolve("OrderEvent.java"), """
                package com.example;
                public enum OrderEvent { PAY }
                """);
        Files.writeString(javaRoot.resolve("OrderState.java"), """
                package com.example;
                public enum OrderState { NEW, PAID }
                """);
        Files.writeString(javaRoot.resolve("DocumentEvent.java"), """
                package com.example;
                public enum DocumentEvent { SUBMIT }
                """);
        Files.writeString(javaRoot.resolve("DocumentState.java"), """
                package com.example;
                public enum DocumentState { DRAFT, SUBMITTED }
                """);
        Files.writeString(javaRoot.resolve("SharedService.java"), """
                package com.example;
                public abstract class SharedService<S, E> {
                    private org.springframework.statemachine.StateMachine<S, E> sm;
                    protected void updateState(E event) {
                        sm.sendEvent(event);
                    }
                }
                """);
        Files.writeString(javaRoot.resolve("OrderService.java"), """
                package com.example;
                public class OrderService extends SharedService<OrderState, OrderEvent> {
                    public void pay() {
                        updateState(OrderEvent.PAY);
                    }
                }
                """);
        Files.writeString(javaRoot.resolve("DocumentService.java"), """
                package com.example;
                public class DocumentService extends SharedService<DocumentState, DocumentEvent> {
                    public void submit() {
                        updateState(DocumentEvent.SUBMIT);
                    }
                }
                """);
        Files.writeString(javaRoot.resolve("OrderController.java"), """
                package com.example;
                public class OrderController {
                    private final OrderService orderService;
                    public OrderController(OrderService orderService) {
                        this.orderService = orderService;
                    }
                    public void pay() {
                        orderService.pay();
                    }
                }
                """);
        Files.writeString(javaRoot.resolve("DocumentController.java"), """
                package com.example;
                public class DocumentController {
                    private final DocumentService documentService;
                    public DocumentController(DocumentService documentService) {
                        this.documentService = documentService;
                    }
                    public void submit() {
                        documentService.submit();
                    }
                }
                """);
        Files.writeString(javaRoot.resolve("OrderStateMachineConfig.java"), """
                package com.example;
                @org.springframework.statemachine.config.EnableStateMachine
                public class OrderStateMachineConfig
                        extends org.springframework.statemachine.config.StateMachineConfigurerAdapter<OrderState, OrderEvent> {}
                """);
        Files.writeString(javaRoot.resolve("DocumentStateMachineConfig.java"), """
                package com.example;
                @org.springframework.statemachine.config.EnableStateMachine
                public class DocumentStateMachineConfig
                        extends org.springframework.statemachine.config.StateMachineConfigurerAdapter<DocumentState, DocumentEvent> {}
                """);
    }

    private static Transition transition(String simple, String fqn, String from, String to) {
        Transition t = new Transition();
        t.setSourceStates(List.of(State.of(from, from)));
        t.setTargetStates(List.of(State.of(to, to)));
        t.setEvent(Event.of(simple, fqn));
        return t;
    }
}
