package click.kamil.springstatemachineexporter.analysis.flow.alloc;

import click.kamil.springstatemachineexporter.analysis.model.CallEdge;
import click.kamil.springstatemachineexporter.analysis.pipeline.AccessorResolver;
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
import click.kamil.springstatemachineexporter.analysis.service.ConstantExtractor;
import click.kamil.springstatemachineexporter.analysis.service.ConstructorAnalyzer;
import click.kamil.springstatemachineexporter.analysis.service.VariableTracer;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.MethodInvocation;
import org.eclipse.jdt.core.dom.TypeDeclaration;
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.HashMap;
import java.util.List;
import java.util.Map;

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

class AllocationSensitiveGetTypeTest {

    @TempDir
    Path tempDir;

    @Test
    void sameMethodCicReceiverResolvesUniqueGetTypeConstant() throws Exception {
        writeBaseFixture();
        Files.writeString(tempDir.resolve("Use.java"), """
                package com.example;
                class LocalUse {
                    void run() {
                        RichEvent e = new PayRichEvent();
                        String t = e.getType();
                    }
                }
                """);

        CodebaseContext context = scan();
        AllocationFactPropagator propagator = propagator(context);
        VirtualAccessorConstResolver resolver = resolver(context);

        TypeDeclaration td = context.getTypeDeclaration("com.example.LocalUse");
        MethodDeclaration run = context.findMethodDeclaration(td, "run", true);
        MethodInvocation getType = findInvocation(run, "getType");

        ReceiverFacts facts = propagator.factsForAccessorReceiver(getType, List.of(), Map.of());
        assertThat(facts.isUnique()).isTrue();
        assertThat(resolver.resolveUniqueConstant(facts, "getType"))
                .containsExactly("PAY");
    }

    @Test
    void oneHopParamForwardResolvesUniqueGetTypeNameConstant() throws Exception {
        writeBaseFixture();
        Files.writeString(tempDir.resolve("Dispatch.java"), """
                package com.example;
                class Api {
                    Dispatcher dispatcher;
                    void pay() { dispatcher.fire(new PayRichEvent()); }
                }
                class Dispatcher {
                    void fire(RichEvent e) {
                        String t = e.getType();
                    }
                }
                """);

        CodebaseContext context = scan();
        AllocationFactPropagator propagator = propagator(context);
        VirtualAccessorConstResolver resolver = resolver(context);

        TypeDeclaration dispatcher = context.getTypeDeclaration("com.example.Dispatcher");
        MethodDeclaration fire = context.findMethodDeclaration(dispatcher, "fire", true);
        MethodInvocation getType = findInvocation(fire, "getType");

        Map<String, List<CallEdge>> graph = new HashMap<>();
        graph.put("com.example.Api.pay", List.of(
                new CallEdge("com.example.Dispatcher.fire", List.of("new PayRichEvent()"))));
        List<String> path = List.of("com.example.Api.pay", "com.example.Dispatcher.fire");

        ReceiverFacts facts = propagator.factsForAccessorReceiver(getType, path, graph);
        assertThat(facts.isUnique()).isTrue();
        assertThat(resolver.resolveUniqueConstant(facts, "getType"))
                .containsExactly("PAY");
    }

    @Test
    void multiImplWithoutAllocationStaysEmpty() throws Exception {
        writeBaseFixture();
        Files.writeString(tempDir.resolve("Shared.java"), """
                package com.example;
                class SharedDispatcher {
                    RichEvent carrier;
                    void fire() {
                        String t = carrier.getType();
                    }
                }
                """);

        CodebaseContext context = scan();
        AllocationFactPropagator propagator = propagator(context);
        VirtualAccessorConstResolver resolver = resolver(context);

        TypeDeclaration td = context.getTypeDeclaration("com.example.SharedDispatcher");
        MethodDeclaration fire = context.findMethodDeclaration(td, "fire", true);
        MethodInvocation getType = findInvocation(fire, "getType");

        ReceiverFacts facts = propagator.factsForAccessorReceiver(getType, List.of(), Map.of());
        assertThat(facts.isEmpty()).isTrue();
        assertThat(resolver.resolveUniqueConstant(facts, "getType")).isEmpty();
    }

    @Test
    void factoryReturnResolvesUniqueGetTypeConstant() throws Exception {
        writeBaseFixture();
        Files.writeString(tempDir.resolve("Factory.java"), """
                package com.example;
                class Events {
                    static RichEvent pay() { return new PayRichEvent(); }
                }
                class Api {
                    Dispatcher dispatcher;
                    void pay() { dispatcher.fire(Events.pay()); }
                }
                class Dispatcher {
                    void fire(RichEvent e) {
                        String t = e.getType();
                    }
                }
                """);

        CodebaseContext context = scan();
        AllocationFactPropagator propagator = propagator(context);
        VirtualAccessorConstResolver resolver = resolver(context);

        TypeDeclaration dispatcher = context.getTypeDeclaration("com.example.Dispatcher");
        MethodDeclaration fire = context.findMethodDeclaration(dispatcher, "fire", true);
        MethodInvocation getType = findInvocation(fire, "getType");

        Map<String, List<CallEdge>> graph = new HashMap<>();
        graph.put("com.example.Api.pay", List.of(
                new CallEdge("com.example.Dispatcher.fire", List.of("Events.pay()"))));
        List<String> path = List.of("com.example.Api.pay", "com.example.Dispatcher.fire");

        ReceiverFacts facts = propagator.factsForAccessorReceiver(getType, path, graph);
        assertThat(facts.isUnique()).isTrue();
        assertThat(resolver.resolveUniqueConstant(facts, "getType"))
                .containsExactly("PAY");
    }

    @Test
    void wrapperGetterPeelResolvesUniqueGetTypeConstant() throws Exception {
        writeBaseFixture();
        Files.writeString(tempDir.resolve("Wrapper.java"), """
                package com.example;
                class EventWrapper {
                    private final RichEvent event;
                    EventWrapper(RichEvent event) { this.event = event; }
                    RichEvent getEvent() { return event; }
                }
                class Api {
                    Dispatcher dispatcher;
                    void pay() { dispatcher.fire(new EventWrapper(new PayRichEvent())); }
                }
                class Dispatcher {
                    void fire(EventWrapper w) {
                        String t = w.getEvent().getType();
                    }
                }
                """);

        CodebaseContext context = scan();
        AllocationFactPropagator propagator = propagator(context);
        VirtualAccessorConstResolver resolver = resolver(context);

        TypeDeclaration dispatcher = context.getTypeDeclaration("com.example.Dispatcher");
        MethodDeclaration fire = context.findMethodDeclaration(dispatcher, "fire", true);
        MethodInvocation getType = findInvocation(fire, "getType");

        Map<String, List<CallEdge>> graph = new HashMap<>();
        graph.put("com.example.Api.pay", List.of(
                new CallEdge("com.example.Dispatcher.fire",
                        List.of("new EventWrapper(new PayRichEvent())"))));
        List<String> path = List.of("com.example.Api.pay", "com.example.Dispatcher.fire");

        ReceiverFacts facts = propagator.factsForAccessorReceiver(getType, path, graph);
        assertThat(facts.isUnique()).isTrue();
        assertThat(resolver.resolveUniqueConstant(facts, "getType"))
                .containsExactly("PAY");
    }

    @Test
    void nonConstantAccessorBodyStaysEmpty() throws Exception {
        writeBaseFixture();
        Files.writeString(tempDir.resolve("Mystery.java"), """
                package com.example;
                class MysteryRichEvent implements RichEvent {
                    public String getType() {
                        return Math.random() > 0.5 ? OrderEvent.PAY.name() : OrderEvent.SHIP.name();
                    }
                }
                class MysteryUse {
                    void run() {
                        RichEvent e = new MysteryRichEvent();
                        String t = e.getType();
                    }
                }
                """);

        CodebaseContext context = scan();
        AllocationFactPropagator propagator = propagator(context);
        VirtualAccessorConstResolver resolver = resolver(context);

        TypeDeclaration td = context.getTypeDeclaration("com.example.MysteryUse");
        MethodDeclaration run = context.findMethodDeclaration(td, "run", true);
        MethodInvocation getType = findInvocation(run, "getType");

        ReceiverFacts facts = propagator.factsForAccessorReceiver(getType, List.of(), Map.of());
        assertThat(facts.isUnique()).isTrue();
        assertThat(resolver.resolveUniqueConstant(facts, "getType")).isEmpty();
    }

    private void writeBaseFixture() throws Exception {
        Files.writeString(tempDir.resolve("Base.java"), """
                package com.example;
                interface RichEvent { String getType(); }
                class PayRichEvent implements RichEvent {
                    public String getType() { return OrderEvent.PAY.name(); }
                }
                class ShipRichEvent implements RichEvent {
                    public String getType() { return OrderEvent.SHIP.name(); }
                }
                enum OrderEvent { PAY, SHIP, CANCEL }
                """);
    }

    private CodebaseContext scan() throws Exception {
        CodebaseContext context = new CodebaseContext();
        context.setResolveBindings(true);
        context.scan(tempDir);
        return context;
    }

    private static AllocationFactPropagator propagator(CodebaseContext context) {
        ConstantResolver constants = context.getConstantResolver();
        VariableTracer tracer = new VariableTracer(context, constants);
        ConstructorAnalyzer ctor = new ConstructorAnalyzer(context, constants);
        return new AllocationFactPropagator(context, tracer, ctor);
    }

    private static VirtualAccessorConstResolver resolver(CodebaseContext context) {
        ConstantResolver constants = context.getConstantResolver();
        ConstantExtractor extractor = new ConstantExtractor(context, constants, mi -> null);
        ConstructorAnalyzer ctor = new ConstructorAnalyzer(context, constants);
        AccessorResolver accessors = new AccessorResolver(context, extractor, ctor, constants);
        return new VirtualAccessorConstResolver(context, accessors, constants);
    }

    private static MethodInvocation findInvocation(MethodDeclaration method, String name) {
        MethodInvocation[] found = {null};
        method.accept(new org.eclipse.jdt.core.dom.ASTVisitor() {
            @Override
            public boolean visit(MethodInvocation node) {
                if (name.equals(node.getName().getIdentifier())) {
                    found[0] = node;
                }
                return super.visit(node);
            }
        });
        assertThat(found[0]).as("expected invocation %s", name).isNotNull();
        return found[0];
    }
}
