package click.kamil.springstatemachineexporter.analysis.service;

import click.kamil.springstatemachineexporter.analysis.enricher.TransitionLinkerEnricher;
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.analysis.spring.InjectionPointAnalyzer;
import click.kamil.springstatemachineexporter.analysis.spring.SpringBeanRegistry;
import click.kamil.springstatemachineexporter.analysis.spring.SpringContextScanner;
import click.kamil.springstatemachineexporter.analysis.spring.SpringDependencyResolver;
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.eclipse.jdt.core.dom.CompilationUnit;
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.Set;

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

/**
 * Unique {@code @Component} implementing an interface field acts as CIC substitute for EIR.
 */
class BeanAsAllocEventIdentityTest {

    @Test
    void uniqueComponentBeanAsAllocResolves(@TempDir Path tempDir) throws Exception {
        Files.writeString(tempDir.resolve("App.java"), """
                package com.example;
                import org.springframework.stereotype.Component;
                public class ApiController {
                    Dispatcher dispatcher;
                    public void pay() { dispatcher.fire(); }
                }
                class Dispatcher {
                    StateMachine machine;
                    RichEvent carrier;
                    void fire() {
                        machine.sendEvent(OrderEvent.valueOf(carrier.getA()));
                    }
                }
                interface RichEvent { String getA(); }
                @Component
                class PayRichEvent implements RichEvent {
                    public String getA() { return OrderEvent.PAY.name(); }
                }
                class ShipRichEvent implements RichEvent {
                    public String getA() { return OrderEvent.SHIP.name(); }
                }
                class StateMachine { public void sendEvent(OrderEvent e) {} }
                enum OrderEvent { PAY, SHIP, CANCEL }
                enum OrderState { NEW, PAID, SHIPPED, CANCELLED }
                class OrderStateMachineConfig {}
                """);

        CodebaseContext context = new CodebaseContext();
        context.setResolveBindings(true);
        context.setClasspath(ToolingClasspath.currentJvmJarEntries());
        context.scan(Set.of(tempDir), Set.of());

        SpringBeanRegistry registry = new SpringBeanRegistry();
        for (CompilationUnit cu : context.getCompilationUnits()) {
            cu.accept(new SpringContextScanner(registry));
        }
        InjectionPointAnalyzer injectionAnalyzer =
                new InjectionPointAnalyzer(new SpringDependencyResolver(registry));
        JdtCallGraphEngine engine = new JdtCallGraphEngine(context, injectionAnalyzer);

        EntryPoint entry = EntryPoint.builder()
                .className("com.example.ApiController")
                .methodName("pay")
                .build();
        TriggerPoint trigger = TriggerPoint.builder()
                .className("com.example.StateMachine")
                .methodName("sendEvent")
                .event("e")
                .build();
        List<CallChain> chains = engine.findChains(List.of(entry), List.of(trigger));
        assertThat(chains).hasSize(1);

        Transition pay = transition("NEW", "PAID", "com.example.OrderEvent.PAY");
        Transition ship = transition("PAID", "SHIPPED", "com.example.OrderEvent.SHIP");
        AnalysisResult result = AnalysisResult.builder()
                .name("com.example.OrderStateMachineConfig")
                .transitions(List.of(pay, ship))
                .metadata(CodebaseMetadata.builder().callChains(chains).build())
                .build();
        new TransitionLinkerEnricher().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 multiComponentBeanStaysFailClosed(@TempDir Path tempDir) throws Exception {
        Files.writeString(tempDir.resolve("App.java"), """
                package com.example;
                import org.springframework.stereotype.Component;
                public class ApiController {
                    Dispatcher dispatcher;
                    public void pay() { dispatcher.fire(); }
                }
                class Dispatcher {
                    StateMachine machine;
                    RichEvent carrier;
                    void fire() {
                        machine.sendEvent(OrderEvent.valueOf(carrier.getA()));
                    }
                }
                interface RichEvent { String getA(); }
                @Component
                class PayRichEvent implements RichEvent {
                    public String getA() { return OrderEvent.PAY.name(); }
                }
                @Component
                class ShipRichEvent implements RichEvent {
                    public String getA() { return OrderEvent.SHIP.name(); }
                }
                class StateMachine { public void sendEvent(OrderEvent e) {} }
                enum OrderEvent { PAY, SHIP, CANCEL }
                enum OrderState { NEW, PAID, SHIPPED, CANCELLED }
                class OrderStateMachineConfig {}
                """);

        CodebaseContext context = new CodebaseContext();
        context.setResolveBindings(true);
        context.setClasspath(ToolingClasspath.currentJvmJarEntries());
        context.scan(Set.of(tempDir), Set.of());

        SpringBeanRegistry registry = new SpringBeanRegistry();
        for (CompilationUnit cu : context.getCompilationUnits()) {
            cu.accept(new SpringContextScanner(registry));
        }
        InjectionPointAnalyzer injectionAnalyzer =
                new InjectionPointAnalyzer(new SpringDependencyResolver(registry));
        JdtCallGraphEngine engine = new JdtCallGraphEngine(context, injectionAnalyzer);

        EntryPoint entry = EntryPoint.builder()
                .className("com.example.ApiController")
                .methodName("pay")
                .build();
        TriggerPoint trigger = TriggerPoint.builder()
                .className("com.example.StateMachine")
                .methodName("sendEvent")
                .event("e")
                .build();
        List<CallChain> chains = engine.findChains(List.of(entry), List.of(trigger));
        assertThat(chains).hasSize(1);

        Transition pay = transition("NEW", "PAID", "com.example.OrderEvent.PAY");
        Transition ship = transition("PAID", "SHIPPED", "com.example.OrderEvent.SHIP");
        AnalysisResult result = AnalysisResult.builder()
                .name("com.example.OrderStateMachineConfig")
                .transitions(List.of(pay, ship))
                .metadata(CodebaseMetadata.builder().callChains(chains).build())
                .build();
        new TransitionLinkerEnricher().enrich(result, null, null);
        CallChain linked = result.getMetadata().getCallChains().get(0);
        assertThat(linked.getMatchedTransitions()).isNullOrEmpty();
        assertThat(linked.getLinkResolution())
                .isIn(LinkResolution.AMBIGUOUS_WIDEN, LinkResolution.NO_MATCH, LinkResolution.UNRESOLVED_EXTERNAL);
    }

    @Test
    void qualifierDisambiguatesMultiComponent(@TempDir Path tempDir) throws Exception {
        Files.writeString(tempDir.resolve("App.java"), """
                package com.example;
                import org.springframework.beans.factory.annotation.Qualifier;
                import org.springframework.stereotype.Component;
                public class ApiController {
                    Dispatcher dispatcher;
                    public void pay() { dispatcher.fire(); }
                }
                class Dispatcher {
                    StateMachine machine;
                    @Qualifier("pay")
                    RichEvent carrier;
                    void fire() {
                        machine.sendEvent(OrderEvent.valueOf(carrier.getA()));
                    }
                }
                interface RichEvent { String getA(); }
                @Component
                @Qualifier("pay")
                class PayRichEvent implements RichEvent {
                    public String getA() { return OrderEvent.PAY.name(); }
                }
                @Component
                @Qualifier("ship")
                class ShipRichEvent implements RichEvent {
                    public String getA() { return OrderEvent.SHIP.name(); }
                }
                class StateMachine { public void sendEvent(OrderEvent e) {} }
                enum OrderEvent { PAY, SHIP, CANCEL }
                enum OrderState { NEW, PAID, SHIPPED, CANCELLED }
                class OrderStateMachineConfig {}
                """);

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

    @Test
    void uniqueBeanFactoryMethodAsAllocResolves(@TempDir Path tempDir) throws Exception {
        Files.writeString(tempDir.resolve("App.java"), """
                package com.example;
                import org.springframework.context.annotation.Bean;
                import org.springframework.context.annotation.Configuration;
                public class ApiController {
                    Dispatcher dispatcher;
                    public void pay() { dispatcher.fire(); }
                }
                class Dispatcher {
                    StateMachine machine;
                    RichEvent carrier;
                    void fire() {
                        machine.sendEvent(OrderEvent.valueOf(carrier.getA()));
                    }
                }
                interface RichEvent { String getA(); }
                class PayRichEvent implements RichEvent {
                    public String getA() { return OrderEvent.PAY.name(); }
                }
                class ShipRichEvent implements RichEvent {
                    public String getA() { return OrderEvent.SHIP.name(); }
                }
                @Configuration
                class EventConfig {
                    @Bean
                    RichEvent payCarrier() {
                        return new PayRichEvent();
                    }
                }
                class StateMachine { public void sendEvent(OrderEvent e) {} }
                enum OrderEvent { PAY, SHIP, CANCEL }
                enum OrderState { NEW, PAID, SHIPPED, CANCELLED }
                class OrderStateMachineConfig {}
                """);

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

    private static CallChain linkWithInjection(Path tempDir) throws Exception {
        CodebaseContext context = new CodebaseContext();
        context.setResolveBindings(true);
        context.setClasspath(ToolingClasspath.currentJvmJarEntries());
        context.scan(Set.of(tempDir), Set.of());

        SpringBeanRegistry registry = new SpringBeanRegistry();
        for (CompilationUnit cu : context.getCompilationUnits()) {
            cu.accept(new SpringContextScanner(registry));
        }
        InjectionPointAnalyzer injectionAnalyzer =
                new InjectionPointAnalyzer(new SpringDependencyResolver(registry));
        JdtCallGraphEngine engine = new JdtCallGraphEngine(context, injectionAnalyzer);

        EntryPoint entry = EntryPoint.builder()
                .className("com.example.ApiController")
                .methodName("pay")
                .build();
        TriggerPoint trigger = TriggerPoint.builder()
                .className("com.example.StateMachine")
                .methodName("sendEvent")
                .event("e")
                .build();
        List<CallChain> chains = engine.findChains(List.of(entry), List.of(trigger));
        assertThat(chains).hasSize(1);

        Transition pay = transition("NEW", "PAID", "com.example.OrderEvent.PAY");
        Transition ship = transition("PAID", "SHIPPED", "com.example.OrderEvent.SHIP");
        AnalysisResult result = AnalysisResult.builder()
                .name("com.example.OrderStateMachineConfig")
                .transitions(List.of(pay, ship))
                .metadata(CodebaseMetadata.builder().callChains(chains).build())
                .build();
        new TransitionLinkerEnricher().enrich(result, null, null);
        return result.getMetadata().getCallChains().get(0);
    }

    private static Transition transition(String source, String target, String eventFqn) {
        Transition transition = new Transition();
        transition.setSourceStates(List.of(State.of(source, source)));
        transition.setTargetStates(List.of(State.of(target, target)));
        transition.setEvent(Event.of(eventFqn, eventFqn));
        return transition;
    }
}
