package click.kamil.springstatemachineexporter.analysis.service;

import click.kamil.springstatemachineexporter.analysis.index.AccessorSummary;
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import click.kamil.springstatemachineexporter.ast.common.DataFlowModel;
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.TypeDeclaration;
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
import org.eclipse.jdt.core.dom.VariableDeclarationStatement;
import org.junit.jupiter.api.Nested;
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.List;
import java.util.Optional;

import static click.kamil.springstatemachineexporter.analysis.service.CentralDispatcherTestSupport.*;
import static org.assertj.core.api.Assertions.assertThat;

/**
 * Quirk-focused tests for accessor inlining edge cases: super dispatch, field-backed chains,
 * inheritance, and look-alike patterns that must not be confused.
 */
class AccessorInliningEdgeCasesTest {

    @Nested
    class SuperMethodAccessorShortcut {

        @Test
        void shouldReadFieldDeclaredOnGrandparent(@TempDir Path tempDir) throws IOException {
            writeJava(tempDir, "com/example/Grand.java", """
                    package com.example;
                    public class Grand {
                        protected String token = "GRAND";
                        public String getToken() { return token; }
                    }
                    """);
            writeJava(tempDir, "com/example/Child.java", """
                    package com.example;
                    public class Child extends Grand {
                        public String capture() {
                            return super.getToken();
                        }
                    }
                    """);
            writeJava(tempDir, "com/example/Runner.java", """
                    package com.example;
                    public class Runner {
                        public void run() {
                            Child child = new Child();
                            String value = child.capture();
                        }
                    }
                    """);

            assertResolved(tempDir, "value", "GRAND");
        }

        @Test
        void shouldNotShortcutWhenSuperGetterIsNonTrivial(@TempDir Path tempDir) throws IOException {
            writeJava(tempDir, "com/example/Base.java", """
                    package com.example;
                    public class Base {
                        protected String event = "IGNORED";
                        public String getEvent() {
                            return event == null ? "NONE" : event;
                        }
                    }
                    """);
            writeJava(tempDir, "com/example/Child.java", """
                    package com.example;
                    public class Child extends Base {
                        public String capture() {
                            return super.getEvent();
                        }
                    }
                    """);
            writeJava(tempDir, "com/example/Runner.java", """
                    package com.example;
                    public class Runner {
                        public void run() {
                            Child child = new Child();
                            String value = child.capture();
                        }
                    }
                    """);

            assertThat(accessorLookup(tempDir, "com.example.Base", "getEvent")).isEmpty();
        }

        @Test
        void callGraphShouldPreferSuperFieldOverChildOverride(@TempDir Path tempDir) throws Exception {
            String source = """
                package com.example;
                public class ApiController {
                    ChildController controller;
                    public void run() {
                        controller.process();
                    }
                }
                class ChildController extends ParentController {
                    @Override
                    public OrderEvent getEvent() { return OrderEvent.SHIP; }
                    public void process() {
                        StateMachine sm = new StateMachine();
                        sm.sendEvent(super.getEvent());
                    }
                }
                class ParentController {
                    private OrderEvent event = OrderEvent.PAY;
                    public OrderEvent getEvent() { return event; }
                }
                enum OrderEvent { PAY, SHIP }
                class StateMachine { public void sendEvent(OrderEvent e) {} }
                class OrderStateMachineConfig {}
                """;
            CodebaseContext context = scanSource(source, tempDir);
            CallChain chain = resolveChain(context, "com.example.ApiController", "run", "com.example.StateMachine");
            assertPolyEvents(chain, "OrderEvent.PAY");
        }
    }

    @Nested
    class FieldBackedGetterChains {

        @Test
        void shouldResolveDeepFieldBackedChainWithoutHopLimit(@TempDir Path tempDir) throws Exception {
            String source = """
                package com.example;
                public class ApiController {
                    CentralDispatcher dispatcher;
                    public void pay(Order order) {
                        dispatcher.fire(order);
                    }
                }
                class CentralDispatcher {
                    public void fire(Order order) {
                        StateMachine sm = new StateMachine();
                        sm.sendEvent(order.getPayload().getEvent().getCode());
                    }
                }
                class Order {
                    private Payload payload = new Payload();
                    public Payload getPayload() { return payload; }
                }
                class Payload {
                    private Event event = new Event();
                    public Event getEvent() { return event; }
                }
                class Event {
                    private OrderEvent code = OrderEvent.PAY;
                    public OrderEvent getCode() { return code; }
                }
                enum OrderEvent { PAY, SHIP }
                class StateMachine { public void sendEvent(OrderEvent e) {} }
                class OrderStateMachineConfig {}
                """;
            CodebaseContext context = scanSource(source, tempDir);
            CallChain chain = resolveChain(context, "com.example.ApiController", "pay", "com.example.StateMachine");
            assertPolyEvents(chain, "OrderEvent.PAY");
        }

        @Test
        void shouldResolveFieldInitializerDeclaredOnSuperclass(@TempDir Path tempDir) throws Exception {
            String source = """
                package com.example;
                public class ApiController {
                    CentralDispatcher dispatcher;
                    public void pay(BaseWrapper wrapper) {
                        dispatcher.fire(wrapper);
                    }
                }
                class CentralDispatcher {
                    public void fire(BaseWrapper wrapper) {
                        StateMachine sm = new StateMachine();
                        sm.sendEvent(wrapper.getPayload().getType());
                    }
                }
                class BaseWrapper {
                    protected Payload payload = new Payload();
                    public Payload getPayload() { return payload; }
                }
                class Payload {
                    private OrderEvent type = OrderEvent.PAY;
                    public OrderEvent getType() { return type; }
                }
                enum OrderEvent { PAY, SHIP }
                class StateMachine { public void sendEvent(OrderEvent e) {} }
                class OrderStateMachineConfig {}
                """;
            CodebaseContext context = scanSource(source, tempDir);
            CallChain chain = resolveChain(context, "com.example.ApiController", "pay", "com.example.StateMachine");
            assertPolyEvents(chain, "OrderEvent.PAY");
        }

        @Test
        void shouldNotUseIndexedFieldWhenInnerGetterReturnsLiteral(@TempDir Path tempDir) throws Exception {
            String source = """
                package com.example;
                public class ApiController {
                    CentralDispatcher dispatcher;
                    public void pay(EventWrapper wrapper) {
                        dispatcher.fire(wrapper);
                    }
                }
                class CentralDispatcher {
                    public void fire(EventWrapper wrapper) {
                        StateMachine sm = new StateMachine();
                        sm.sendEvent(wrapper.getEvent().getType());
                    }
                }
                class EventWrapper {
                    private RichEvent event = new RichEvent();
                    public RichEvent getEvent() { return event; }
                }
                class RichEvent {
                    private OrderEvent type = OrderEvent.PAY;
                    public OrderEvent getType() {
                        return OrderEvent.SHIP;
                    }
                }
                enum OrderEvent { PAY, SHIP }
                class StateMachine { public void sendEvent(OrderEvent e) {} }
                class OrderStateMachineConfig {}
                """;
            CodebaseContext context = scanSource(source, tempDir);
            CallChain chain = resolveChain(context, "com.example.ApiController", "pay", "com.example.StateMachine");
            assertPolyEvents(chain, "OrderEvent.SHIP");
        }

        @Test
        void shouldDistinguishFieldBackedChainFromLiteralReturnChain(@TempDir Path tempDir) throws Exception {
            String literalSource = """
                package com.example;
                public class ApiController {
                    CentralDispatcher dispatcher;
                    public void pay(EventWrapper wrapper) {
                        dispatcher.fire(wrapper);
                    }
                }
                class CentralDispatcher {
                    public void fire(EventWrapper wrapper) {
                        StateMachine sm = new StateMachine();
                        sm.sendEvent(wrapper.getEvent().getType());
                    }
                }
                class EventWrapper {
                    public RichEvent getEvent() { return new RichEvent(); }
                }
                class RichEvent {
                    public OrderEvent getType() { return OrderEvent.PAY; }
                }
                enum OrderEvent { PAY, SHIP }
                class StateMachine { public void sendEvent(OrderEvent e) {} }
                class OrderStateMachineConfig {}
                """;
            Path literalDir = tempDir.resolve("literal");
            Files.createDirectories(literalDir);
            CodebaseContext literalContext = scanSource(literalSource, literalDir);
            CallChain literalChain = resolveChain(
                    literalContext, "com.example.ApiController", "pay", "com.example.StateMachine");
            assertPolyEvents(literalChain, "OrderEvent.PAY");

            String fieldSource = """
                package com.example;
                public class ApiController {
                    CentralDispatcher dispatcher;
                    public void pay(EventWrapper wrapper) {
                        dispatcher.fire(wrapper);
                    }
                }
                class CentralDispatcher {
                    public void fire(EventWrapper wrapper) {
                        StateMachine sm = new StateMachine();
                        sm.sendEvent(wrapper.getEvent().getType());
                    }
                }
                class EventWrapper {
                    private RichEvent event = new RichEvent();
                    public RichEvent getEvent() { return event; }
                }
                class RichEvent {
                    private OrderEvent type = OrderEvent.PAY;
                    public OrderEvent getType() { return type; }
                }
                enum OrderEvent { PAY, SHIP }
                class StateMachine { public void sendEvent(OrderEvent e) {} }
                class OrderStateMachineConfig {}
                """;
            Path fieldDir = tempDir.resolve("field");
            Files.createDirectories(fieldDir);
            CodebaseContext fieldContext = scanSource(fieldSource, fieldDir);
            CallChain fieldChain = resolveChain(
                    fieldContext, "com.example.ApiController", "pay", "com.example.StateMachine");
            assertPolyEvents(fieldChain, "OrderEvent.PAY");
            assertLinkedEvent(
                    linkEndpointToTransition(fieldChain, "com.example.OrderStateMachineConfig",
                            transition("NEW", "PAID", "OrderEvent.PAY"),
                            transition("PAID", "SHIPPED", "OrderEvent.SHIP")),
                    "OrderEvent.PAY");
        }
    }

    @Nested
    class LookAlikePatterns {

        @Test
        void shouldNotTreatListGetAsBeanAccessorInDataflow(@TempDir Path tempDir) throws IOException {
            writeJava(tempDir, "com/example/Runner.java", """
                    package com.example;
                    import java.util.List;
                    public class Runner {
                        public void run() {
                            List<String> events = List.of("A", "B");
                            String value = events.get(0);
                        }
                    }
                    """);

            CodebaseContext context = scan(tempDir);
            assertThat(context.getAccessorIndex().lookup("java.util.List", "get")).isEmpty();
            DataFlowModel model = new JdtDataFlowModel(context);
            Expression initializer = findInitializer(context, "run", "value");
            assertThat(model.getReachingDefinitions(initializer)).isNotEmpty();
        }

        @Test
        void shouldNotIndexBareGetMethod(@TempDir Path tempDir) throws IOException {
            writeJava(tempDir, "com/example/Runner.java", """
                    package com.example;
                    public class Runner {
                        static class Box {
                            public String get() { return "X"; }
                        }
                        public void run() {
                            Box box = new Box();
                            String value = box.get();
                        }
                    }
                    """);

            assertThat(accessorLookup(tempDir, "com.example.Runner.Box", "get")).isEmpty();
            assertResolved(tempDir, "value", "X");
        }
    }

    private static void assertResolved(Path tempDir, String variableName, String expected) throws IOException {
        CodebaseContext context = scan(tempDir);
        DataFlowModel model = new JdtDataFlowModel(context);
        Expression initializer = findInitializer(context, "run", variableName);
        String resolved = model.resolveValue(initializer, context);
        assertThat(resolved).isEqualTo(expected);
    }

    private static Optional<AccessorSummary> accessorLookup(Path tempDir, String ownerFqn, String methodName)
            throws IOException {
        return scan(tempDir).getAccessorIndex().lookup(ownerFqn, methodName);
    }

    private static CodebaseContext scan(Path tempDir) throws IOException {
        CodebaseContext context = new CodebaseContext();
        context.setResolveBindings(true);
        context.setSourcepath(List.of(tempDir.toAbsolutePath().toString()));
        context.scan(tempDir);
        return context;
    }

    private static Expression findInitializer(CodebaseContext context, String methodName, String variableName) {
        for (TypeDeclaration type : context.getTypeDeclarations()) {
            Expression result = findInitializerInType(type, methodName, variableName);
            if (result != null) {
                return result;
            }
        }
        throw new AssertionError("Initializer not found for " + variableName + " in " + methodName);
    }

    private static Expression findInitializerInType(TypeDeclaration type, String methodName, String variableName) {
        for (MethodDeclaration method : type.getMethods()) {
            if (!method.getName().getIdentifier().equals(methodName) || method.getBody() == null) {
                continue;
            }
            for (Object statementObj : method.getBody().statements()) {
                if (statementObj instanceof VariableDeclarationStatement statement) {
                    for (Object fragmentObj : statement.fragments()) {
                        if (fragmentObj instanceof VariableDeclarationFragment fragment
                                && fragment.getName().getIdentifier().equals(variableName)) {
                            return fragment.getInitializer();
                        }
                    }
                }
            }
        }
        for (TypeDeclaration nested : type.getTypes()) {
            Expression result = findInitializerInType(nested, methodName, variableName);
            if (result != null) {
                return result;
            }
        }
        return null;
    }

    private static void writeJava(Path tempDir, String relativePath, String source) throws IOException {
        Path file = tempDir.resolve(relativePath);
        Files.createDirectories(file.getParent());
        Files.writeString(file, source);
    }
}
