package click.kamil.springstatemachineexporter.analysis.service;

import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
import click.kamil.springstatemachineexporter.ast.common.AstUtils;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import click.kamil.springstatemachineexporter.ast.common.JdtDataFlowModel;
import org.eclipse.jdt.core.dom.Expression;
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.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;

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

/**
 * Proves {@link JdtDataFlowModel} + anchored source AST resolves getter chains that detached
 * re-parsing cannot, without relying on the call-graph accessor fallback tier.
 */
class DataFlowGetterEnablesMoreTest {

    private CodebaseContext context;
    private VariableTracer variableTracer;
    private ConstantExtractor constantExtractor;
    private JdtDataFlowModel dataFlowModel;

    @BeforeEach
    void setUp(@TempDir Path tempDir) throws IOException {
        context = new CodebaseContext();
        context.setResolveBindings(true);
        context.scan(tempDir);
        ConstantResolver constantResolver = context.getConstantResolver();
        variableTracer = new VariableTracer(context, constantResolver);
        constantExtractor = new ConstantExtractor(context, constantResolver, null);
        variableTracer.setConstantExtractor(constantExtractor);
        dataFlowModel = new JdtDataFlowModel(context);
    }

    @Test
    void shouldResolveChainedParameterGetterViaAnchoredDataflow(@TempDir Path tempDir) throws IOException {
        scan(tempDir, """
                package com.example;
                public class OrderController {
                    public void processOrderEvent(EventWrapper wrapper) {
                        wrapper.getEvent().getType();
                    }
                }
                class EventWrapper {
                    public RichEvent getEvent() { return new RichEvent(); }
                }
                class RichEvent {
                    public OrderEvents getType() { return OrderEvents.PAY; }
                }
                enum OrderEvents { PAY }
                """);

        String methodFqn = "com.example.OrderController.processOrderEvent";
        String expressionText = "wrapper.getEvent().getType()";

        assertThat(resolveViaDataflow(expressionText, methodFqn))
                .anyMatch(value -> value.contains("PAY"));

        assertThat(resolveViaDetachedParse(expressionText))
                .isEmpty();
    }

    @Test
    void shouldResolveSuperGetterFieldViaAnchoredDataflow(@TempDir Path tempDir) throws IOException {
        scan(tempDir, """
                package com.example;
                class ChildController extends ParentController {
                    void process() {
                        super.getEvent();
                    }
                }
                class ParentController {
                    private OrderEvent event = OrderEvent.PAY;
                    OrderEvent getEvent() { return event; }
                }
                enum OrderEvent { PAY, SHIP }
                """);

        String methodFqn = "com.example.ChildController.process";
        String expressionText = "super.getEvent()";

        assertThat(resolveViaDataflow(expressionText, methodFqn))
                .anyMatch(value -> value.contains("PAY"));
    }

    @Test
    void shouldResolveDeepFieldBackedChainViaAnchoredDataflow(@TempDir Path tempDir) throws IOException {
        scan(tempDir, """
                package com.example;
                class CentralDispatcher {
                    void fire(Order order) {
                        order.getPayload().getEvent().getCode();
                    }
                }
                class Order {
                    private Payload payload = new Payload();
                    Payload getPayload() { return payload; }
                }
                class Payload {
                    private Event event = new Event();
                    Event getEvent() { return event; }
                }
                class Event {
                    private OrderEvent code = OrderEvent.PAY;
                    OrderEvent getCode() { return code; }
                }
                enum OrderEvent { PAY, SHIP }
                """);

        String methodFqn = "com.example.CentralDispatcher.fire";
        String expressionText = "order.getPayload().getEvent().getCode()";

        assertThat(resolveViaDataflow(expressionText, methodFqn))
                .anyMatch(value -> value.contains("PAY"));
    }

    @Test
    void shouldResolveInterfaceMiddleHopViaAnchoredDataflow(@TempDir Path tempDir) throws IOException {
        scan(tempDir, """
                package com.example;
                class ApiController {
                    Outer outer;
                    void pay() {
                        outer.getInner().getPayload().getEvent();
                    }
                }
                interface Inner {
                    Payload getPayload();
                }
                class InnerImpl implements Inner {
                    private Payload payload = new Payload();
                    public Payload getPayload() { return payload; }
                }
                class Outer {
                    public Inner getInner() { return new InnerImpl(); }
                }
                class Payload {
                    private OrderEvent event = OrderEvent.PAY;
                    public OrderEvent getEvent() { return event; }
                }
                enum OrderEvent { PAY, SHIP }
                """);

        String methodFqn = "com.example.ApiController.pay";
        String expressionText = "outer.getInner().getPayload().getEvent()";

        assertThat(resolveViaDataflow(expressionText, methodFqn))
                .anyMatch(value -> value.contains("PAY"));
    }

    @Test
    void shouldResolveAnonymousClassGetterViaAnchoredDataflow(@TempDir Path tempDir) throws IOException {
        scan(tempDir, """
                package com.example;
                class Runner {
                    void run() {
                        Handler handler = new Handler() {
                            public OrderEvent getEvent() { return OrderEvent.PAY; }
                        };
                        handler.getEvent();
                    }
                }
                interface Handler {
                    OrderEvent getEvent();
                }
                enum OrderEvent { PAY, SHIP }
                """);

        String methodFqn = "com.example.Runner.run";
        String expressionText = "handler.getEvent()";

        assertThat(resolveViaDataflow(expressionText, methodFqn))
                .anyMatch(value -> value.contains("PAY"));
    }

    @Test
    void shouldResolveSetterBeforeGetterOnParameterViaDataflow(@TempDir Path tempDir) throws IOException {
        scan(tempDir, """
                package com.example;
                class Runner {
                    static class Payload {
                        private String event = "DEFAULT";
                        public void setEvent(String event) { this.event = event; }
                        public String getEvent() { return event; }
                    }
                    void run(Payload payload) {
                        payload.setEvent("PAY");
                        payload.getEvent();
                    }
                }
                """);

        String methodFqn = "com.example.Runner.run";
        String expressionText = "payload.getEvent()";

        assertThat(resolveViaDataflow(expressionText, methodFqn))
                .anyMatch(value -> value.contains("PAY"));
    }

    @Test
    void shouldResolveChainedGetterViaJdtDataFlowModelOnAnchoredAst(@TempDir Path tempDir) throws IOException {
        scan(tempDir, """
                package com.example;
                public class Runner {
                    public void run(EventWrapper wrapper) {
                        OrderEvents value = wrapper.getEvent().getType();
                    }
                }
                class EventWrapper {
                    public RichEvent getEvent() { return new RichEvent(); }
                }
                class RichEvent {
                    public OrderEvents getType() { return OrderEvents.PAY; }
                }
                enum OrderEvents { PAY }
                """);

        MethodInvocation chain = findMethodInvocation(
                "com.example.Runner", "run", "wrapper.getEvent().getType()");
        assertThat(chain).isNotNull();

        String resolved = dataFlowModel.resolveValue(chain, context);
        assertThat(resolved).contains("PAY");

        Expression orphan = AstUtils.parseExpression("wrapper.getEvent().getType()");
        assertThat(dataFlowModel.resolveValue(orphan, context))
                .isEqualTo("wrapper.getEvent().getType()");
    }

    private List<String> resolveViaDataflow(String expressionText, String methodFqn) {
        List<String> searchMethods = List.of(methodFqn);
        List<String> callPath = List.of(methodFqn);
        return variableTracer.resolveConstantsFromSourceText(expressionText, searchMethods, callPath);
    }

    private List<String> resolveViaDetachedParse(String expressionText) {
        Expression orphan = AstUtils.parseExpression(expressionText);
        if (orphan == null) {
            return List.of();
        }
        List<String> constants = new ArrayList<>();
        for (Expression def : dataFlowModel.getReachingDefinitions(orphan)) {
            if (def != null && !def.toString().equals(orphan.toString())) {
                constantExtractor.extractConstantsFromExpression(def, constants);
            }
        }
        return constants;
    }

    private MethodInvocation findMethodInvocation(String classFqn, String methodName, String expressionText) {
        Expression found = AstUtils.findExpressionInMethod(classFqn + "." + methodName, expressionText, context);
        return found instanceof MethodInvocation mi ? mi : null;
    }

    private void scan(Path tempDir, String source) throws IOException {
        Files.writeString(tempDir.resolve("App.java"), source);
        context = new CodebaseContext();
        context.setResolveBindings(true);
        context.scan(tempDir);
        ConstantResolver constantResolver = context.getConstantResolver();
        variableTracer = new VariableTracer(context, constantResolver);
        constantExtractor = new ConstantExtractor(context, constantResolver, null);
        variableTracer.setConstantExtractor(constantExtractor);
        dataFlowModel = new JdtDataFlowModel(context);
    }
}
