package click.kamil.springstatemachineexporter.analysis.service;

import click.kamil.springstatemachineexporter.analysis.model.CallChain;
import click.kamil.springstatemachineexporter.analysis.model.CallEdge;
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
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 org.junit.jupiter.api.BeforeEach;
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 java.util.Set;

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

/**
 * Regression for generic superclass fields supplied via subclass {@code super(concreteService)}.
 */
class ConstructorSuperInjectionNarrowingTest {

    @TempDir
    Path tempDir;

    private CodebaseContext context;
    private JdtCallGraphEngine engine;

    @BeforeEach
    void setUp() throws Exception {
        writeFixture(tempDir);

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

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

    @Test
    void shouldNarrowGenericSuperFieldToConcreteSubclassInjection() {
        Map<String, List<CallEdge>> graph = engine.buildCallGraph();

        List<CallEdge> fromShared = graph.get("com.example.BaseStateMachineService.handle");
        assertThat(fromShared)
                .as("superclass method must resolve collaborator via subclass ctor injection")
                .isNotEmpty();
        assertThat(fromShared)
                .extracting(CallEdge::getTargetMethod)
                .contains("com.example.MosService.processEvent");
        assertThat(fromShared)
                .extracting(CallEdge::getReceiverTypeFqn)
                .containsOnly("com.example.MosService");
        assertThat(fromShared)
                .extracting(CallEdge::getTargetMethod)
                .noneMatch(target -> target.contains("OtherService"));
    }

    @Test
    void shouldProduceSingleChainWithoutSiblingServiceFanOut() {
        List<CallChain> chains = engine.findChains(
                List.of(EntryPoint.builder()
                        .type(EntryPoint.Type.CUSTOM)
                        .className("com.example.MosStateMachineService")
                        .methodName("dispatch")
                        .build()),
                List.of(TriggerPoint.builder()
                        .className("com.example.StateMachine")
                        .methodName("sendEvent")
                        .event("MOS_EVENT")
                        .build()));

        assertThat(chains).isNotEmpty();
        assertThat(chains)
                .allSatisfy(chain -> assertThat(chain.getMethodChain())
                        .noneMatch(hop -> hop.contains("OtherService")));
        assertThat(chains)
                .anyMatch(chain -> chain.getMethodChain().stream()
                        .anyMatch(hop -> hop.contains("MosService")));
    }

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

                import org.springframework.stereotype.Service;

                interface CollaboratorService {
                    void processEvent(String event);
                }

                @Service
                class MosService implements CollaboratorService {
                    private final StateMachine sm = new StateMachine();
                    @Override
                    public void processEvent(String event) {
                        sm.sendEvent("MOS_EVENT");
                    }
                }

                @Service
                class OtherService implements CollaboratorService {
                    private final StateMachine sm = new StateMachine();
                    @Override
                    public void processEvent(String event) {
                        sm.sendEvent("OTHER_EVENT");
                    }
                }

                abstract class BaseStateMachineService<T extends CollaboratorService> {
                    protected final T service;

                    BaseStateMachineService(T service) {
                        this.service = service;
                    }

                    void handle(String event) {
                        service.processEvent(event);
                    }
                }

                @Service
                class MosStateMachineService extends BaseStateMachineService<MosService> {
                    MosStateMachineService(MosService mosService) {
                        super(mosService);
                    }

                    void dispatch(String event) {
                        handle(event);
                    }
                }

                class StateMachine {
                    void sendEvent(Object event) {}
                }
                """);
    }

    @Test
    void shouldPreferBindingOverSameAritySuperOverload() throws Exception {
        Path overloadDir = tempDir.resolve("overload");
        Files.createDirectories(overloadDir);
        Files.writeString(overloadDir.resolve("OverloadSuper.java"), """
                package com.example.overload;

                import org.springframework.stereotype.Service;

                interface CollaboratorService {
                    void processEvent(String event);
                }

                @Service
                class MosService implements CollaboratorService {
                    private final StateMachine sm = new StateMachine();
                    @Override
                    public void processEvent(String event) {
                        sm.sendEvent("MOS_EVENT");
                    }
                }

                @Service
                class OtherService implements CollaboratorService {
                    private final StateMachine sm = new StateMachine();
                    @Override
                    public void processEvent(String event) {
                        sm.sendEvent("OTHER_EVENT");
                    }
                }

                abstract class BaseStateMachineService {
                    protected final CollaboratorService service;

                    // Same arity; service is param 0 here.
                    BaseStateMachineService(CollaboratorService service, String unused) {
                        this.service = service;
                    }

                    // Same arity; service is param 1 here — must not win when binding resolves to the other ctor.
                    BaseStateMachineService(String unused, CollaboratorService service) {
                        this.service = service;
                    }

                    void handle(String event) {
                        service.processEvent(event);
                    }
                }

                @Service
                class MosStateMachineService extends BaseStateMachineService {
                    MosStateMachineService(MosService mosService, String label) {
                        super(mosService, label);
                    }

                    void dispatch(String event) {
                        handle(event);
                    }
                }

                class StateMachine {
                    void sendEvent(Object event) {}
                }
                """);

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

        SpringBeanRegistry registry = new SpringBeanRegistry();
        for (var cu : overloadContext.getCompilationUnits()) {
            cu.accept(new SpringContextScanner(registry));
        }
        InjectionPointAnalyzer injectionAnalyzer =
                new InjectionPointAnalyzer(new SpringDependencyResolver(registry));
        JdtCallGraphEngine overloadEngine = new JdtCallGraphEngine(overloadContext, injectionAnalyzer);

        Map<String, List<CallEdge>> graph = overloadEngine.buildCallGraph();
        List<CallEdge> fromShared = graph.get("com.example.overload.BaseStateMachineService.handle");
        assertThat(fromShared)
                .extracting(CallEdge::getReceiverTypeFqn)
                .containsOnly("com.example.overload.MosService");
        assertThat(fromShared)
                .extracting(CallEdge::getTargetMethod)
                .noneMatch(target -> target.contains("OtherService"));
    }
}
