package click.kamil.springstatemachineexporter.analysis.index;

import click.kamil.springstatemachineexporter.analysis.index.AccessorKind;
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 static org.assertj.core.api.Assertions.assertThat;

class AccessorInliningDataFlowTest {

    @Nested
    class GetterInlining {

        @Test
        void shouldResolveTrivialGetterThroughFieldBinding(@TempDir Path tempDir) throws IOException {
            writeJava(tempDir, "com/example/Runner.java", """
                    package com.example;
                    public class Runner {
                        public static class Payload {
                            private String event = "SHIP";
                            public String getEvent() { return event; }
                        }
                        public void run() {
                            Payload payload = new Payload();
                            String value = payload.getEvent();
                        }
                    }
                    """);

            assertResolvedValue(tempDir, "value", "SHIP");
            assertAccessorIndexed(tempDir, "com.example.Runner.Payload", "getEvent");
        }

        @Test
        void shouldResolveChainedTrivialGetters(@TempDir Path tempDir) throws IOException {
            writeJava(tempDir, "com/example/Runner.java", """
                    package com.example;
                    public class Runner {
                        public static class Event {
                            private String code = "PAY";
                            public String getCode() { return code; }
                        }
                        public static class Payload {
                            private Event event = new Event();
                            public Event getEvent() { return event; }
                        }
                        public static class Order {
                            private Payload payload = new Payload();
                            public Payload getPayload() { return payload; }
                        }
                        public void run() {
                            Order order = new Order();
                            String code = order.getPayload().getEvent().getCode();
                        }
                    }
                    """);

            assertResolvedValue(tempDir, "code", "PAY");
        }

        @Test
        void shouldResolveInheritedFieldThroughChildGetter(@TempDir Path tempDir) throws IOException {
            writeJava(tempDir, "com/example/Base.java", """
                    package com.example;
                    public class Base {
                        protected String event = "BASE";
                    }
                    """);
            writeJava(tempDir, "com/example/Child.java", """
                    package com.example;
                    public class Child extends Base {
                        public String getEvent() { return event; }
                    }
                    """);
            writeJava(tempDir, "com/example/Runner.java", """
                    package com.example;
                    public class Runner {
                        public void run() {
                            Child child = new Child();
                            String value = child.getEvent();
                        }
                    }
                    """);

            assertResolvedValue(tempDir, "value", "BASE");
        }

        @Test
        void shouldResolveSuperTrivialGetterWithoutOpeningSuperBody(@TempDir Path tempDir) throws IOException {
            writeJava(tempDir, "com/example/Base.java", """
                    package com.example;
                    public class Base {
                        protected String event = "SUPER";
                        public String getEvent() { return 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();
                        }
                    }
                    """);

            CodebaseContext context = scan(tempDir);
            DataFlowModel model = new JdtDataFlowModel(context);
            Expression initializer = findInitializer(context, "run", "value");
            String resolved = model.resolveValue(initializer, context);
            assertThat(resolved).isEqualTo("SUPER");
        }

        @Test
        void shouldResolveRecordComponentAfterNonTrivialGetterWrapper(@TempDir Path tempDir) throws IOException {
            writeJava(tempDir, "com/example/Event.java", """
                    package com.example;
                    public record Event(String code) {}
                    """);
            writeJava(tempDir, "com/example/Payload.java", """
                    package com.example;
                    public class Payload {
                        private Event event = new Event("WRAPPED");
                        public Event getEvent() { return event; }
                        public Event getEventViaWrapper() { return getEvent(); }
                    }
                    """);
            writeJava(tempDir, "com/example/Runner.java", """
                    package com.example;
                    public class Runner {
                        public void run() {
                            Payload payload = new Payload();
                            String code = payload.getEventViaWrapper().code();
                        }
                    }
                    """);

            assertResolvedValue(tempDir, "code", "WRAPPED");
        }

        @Test
        void shouldResolveRecordComponentThroughAccessorChain(@TempDir Path tempDir) throws IOException {
            writeJava(tempDir, "com/example/Event.java", """
                    package com.example;
                    public record Event(String code) {}
                    """);
            writeJava(tempDir, "com/example/Payload.java", """
                    package com.example;
                    public class Payload {
                        private Event event = new Event("RECORD");
                        public Event getEvent() { return event; }
                    }
                    """);
            writeJava(tempDir, "com/example/Runner.java", """
                    package com.example;
                    public class Runner {
                        public void run() {
                            Payload payload = new Payload();
                            String code = payload.getEvent().code();
                        }
                    }
                    """);

            assertResolvedValue(tempDir, "code", "RECORD");
        }
    }

    @Nested
    class SetterThenGetter {

        @Test
        void shouldApplySetterMutationBeforeGetterRead(@TempDir Path tempDir) throws IOException {
            writeJava(tempDir, "com/example/Runner.java", """
                    package com.example;
                    public class Runner {
                        public static class Command {
                            private String event = "DEFAULT";
                            public void setEvent(String event) { this.event = event; }
                            public String getEvent() { return this.event; }
                        }
                        public void run() {
                            Command cmd = new Command();
                            cmd.setEvent("PAY");
                            String value = cmd.getEvent();
                        }
                    }
                    """);

            assertResolvedValue(tempDir, "value", "PAY");
        }

        @Test
        void shouldApplySetterThroughInterfaceTypedReceiver(@TempDir Path tempDir) throws IOException {
            writeJava(tempDir, "com/example/Runner.java", """
                    package com.example;
                    public class Runner {
                        public interface Command {
                            void setEvent(String event);
                            String getEvent();
                        }
                        public static class Impl implements Command {
                            private String event = "DEFAULT";
                            @Override
                            public void setEvent(String event) { this.event = event; }
                            @Override
                            public String getEvent() { return event; }
                        }
                        public void run() {
                            Command cmd = new Impl();
                            cmd.setEvent("PAY");
                            String value = cmd.getEvent();
                        }
                    }
                    """);

            assertResolvedValue(tempDir, "value", "PAY");
        }

        @Test
        void shouldResolveSetterArgumentThroughInterfaceTypedReceiver(@TempDir Path tempDir) throws IOException {
            writeJava(tempDir, "com/example/Runner.java", """
                    package com.example;
                    public class Runner {
                        public interface Command {
                            void setEvent(String event);
                            String getEvent();
                        }
                        public static class Impl implements Command {
                            private String event = "DEFAULT";
                            @Override
                            public void setEvent(String event) { this.event = event; }
                            @Override
                            public String getEvent() { return event; }
                        }
                        public static class Helper {
                            public String constant() { return "PAY"; }
                        }
                        public void run() {
                            Command cmd = new Impl();
                            Helper helper = new Helper();
                            cmd.setEvent(helper.constant());
                            String value = cmd.getEvent();
                        }
                    }
                    """);

            assertResolvedValue(tempDir, "value", "PAY");
        }

        @Test
        void shouldResolveFluentSetterChain(@TempDir Path tempDir) throws IOException {
            writeJava(tempDir, "com/example/Runner.java", """
                    package com.example;
                    public class Runner {
                        public static class Builder {
                            private String event = "INIT";
                            public Builder setEvent(String event) {
                                this.event = event;
                                return this;
                            }
                            public String getEvent() { return event; }
                        }
                        public void run() {
                            Builder builder = new Builder();
                            builder.setEvent("FLUENT");
                            String value = builder.getEvent();
                        }
                    }
                    """);

            assertResolvedValue(tempDir, "value", "FLUENT");
        }
    }

    @Nested
    class MustNotBreakExistingResolution {

        @Test
        void shouldStillResolveLiteralReturnWhenGetterIsNotTrivial(@TempDir Path tempDir) throws IOException {
            writeJava(tempDir, "com/example/Runner.java", """
                    package com.example;
                    public class Runner {
                        public static class Service {
                            public String getEvent() { return "LITERAL"; }
                        }
                        public void run() {
                            Service service = new Service();
                            String value = service.getEvent();
                        }
                    }
                    """);

            assertResolvedValue(tempDir, "value", "LITERAL");
            assertThat(accessorLookup(tempDir, "com.example.Runner.Service", "getEvent"))
                    .isPresent()
                    .get()
                    .satisfies(summary -> assertThat(summary.kind()).isEqualTo(AccessorKind.CONSTANT_GETTER));
        }

        @Test
        void shouldStillResolveDynamicDispatchWhenImplementationReturnsLiteral(@TempDir Path tempDir) throws IOException {
            writeJava(tempDir, "com/example/Runner.java", """
                    package com.example;
                    public class Runner {
                        public interface Service {
                            String getEvent();
                        }
                        public static class PayService implements Service {
                            public String getEvent() { return "PAY"; }
                        }
                        public void run() {
                            Service service = new PayService();
                            String value = service.getEvent();
                        }
                    }
                    """);

            assertResolvedValue(tempDir, "value", "PAY");
        }

        @Test
        void shouldNotInlineParentAccessorWhenSubclassOverrideIsBlocked(@TempDir Path tempDir) throws IOException {
            writeJava(tempDir, "com/example/Base.java", """
                    package com.example;
                    public class Base {
                        protected String event = "base";
                        public String getEvent() { return event; }
                    }
                    """);
            writeJava(tempDir, "com/example/Child.java", """
                    package com.example;
                    public class Child extends Base {
                        @Override
                        public String getEvent() {
                            return "child";
                        }
                    }
                    """);
            writeJava(tempDir, "com/example/Runner.java", """
                    package com.example;
                    public class Runner {
                        public void run() {
                            Base target = new Child();
                            String value = target.getEvent();
                        }
                    }
                    """);

            assertResolvedValue(tempDir, "value", "child");
        }

        @Test
        void shouldApplySetterMutationsOnInstanceFieldReceiver(@TempDir Path tempDir) throws IOException {
            writeJava(tempDir, "com/example/Runner.java", """
                    package com.example;
                    public class Runner {
                        private Command command = new Command();
                        public void run() {
                            this.command.setEvent("PAY");
                            String value = this.command.getEvent();
                        }
                        public static class Command {
                            private String event = "DEFAULT";
                            public void setEvent(String event) { this.event = event; }
                            public String getEvent() { return event; }
                        }
                    }
                    """);

            assertResolvedValue(tempDir, "value", "PAY");
        }

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

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

            CodebaseContext context = scan(tempDir);
            DataFlowModel model = new JdtDataFlowModel(context);
            Expression initializer = findInitializer(context, "run", "value");
            String resolved = model.resolveValue(initializer, context);
            assertThat(resolved).isNotEqualTo("BASE");
        }

        @Test
        void shouldNotConfuseMapGetWithBeanGetter(@TempDir Path tempDir) throws IOException {
            writeJava(tempDir, "com/example/Runner.java", """
                    package com.example;
                    import java.util.Map;
                    import java.util.HashMap;
                    public class Runner {
                        public void run() {
                            Map<String, String> map = new HashMap<>();
                            map.put("event", "MAP_VALUE");
                            String value = map.get("event");
                        }
                    }
                    """);

            assertThat(accessorLookup(tempDir, "java.util.HashMap", "get")).isEmpty();
            // Map resolution is separate; ensure we do not crash or return wrong constant.
            CodebaseContext context = scan(tempDir);
            DataFlowModel model = new JdtDataFlowModel(context);
            Expression initializer = findInitializer(context, "run", "value");
            List<Expression> defs = model.getReachingDefinitions(initializer);
            assertThat(defs).isNotEmpty();
        }

        @Test
        void shouldPreserveBranchSensitiveSetterVisibility(@TempDir Path tempDir) throws IOException {
            writeJava(tempDir, "com/example/Runner.java", """
                    package com.example;
                    public class Runner {
                        public static class Command {
                            private String event = "DEFAULT";
                            public void setEvent(String event) { this.event = event; }
                            public String getEvent() { return this.event; }
                        }
                        public void run(boolean pay) {
                            Command cmd = new Command();
                            if (pay) {
                                cmd.setEvent("PAY");
                            } else {
                                cmd.setEvent("CANCEL");
                            }
                            String value = cmd.getEvent();
                        }
                    }
                    """);

            CodebaseContext context = scan(tempDir);
            DataFlowModel model = new JdtDataFlowModel(context);
            Expression initializer = findInitializer(context, "run", "value");
            String resolved = model.resolveValue(initializer, context);
            assertThat(resolved).isIn("PAY", "CANCEL", null);
        }
    }

    @Nested
    class SchemaStressCases {

        @Test
        void shouldHandleDeepGetterChainWithoutStackOverflow(@TempDir Path tempDir) throws IOException {
            writeJava(tempDir, "com/example/Runner.java", """
                    package com.example;
                    public class Runner {
                        public static class L4 { private String v = "DEEP"; public String getV() { return v; } }
                        public static class L3 { private L4 l4 = new L4(); public L4 getL4() { return l4; } }
                        public static class L2 { private L3 l3 = new L3(); public L3 getL3() { return l3; } }
                        public static class L1 { private L2 l2 = new L2(); public L2 getL2() { return l2; } }
                        public void run() {
                            L1 root = new L1();
                            String value = root.getL2().getL3().getL4().getV();
                        }
                    }
                    """);

            assertResolvedValue(tempDir, "value", "DEEP");
        }

        @Test
        void shouldHandleMixedTrivialAndLiteralGetterChain(@TempDir Path tempDir) throws IOException {
            writeJava(tempDir, "com/example/Runner.java", """
                    package com.example;
                    public class Runner {
                        public static class Inner {
                            public String code() { return "INNER"; }
                        }
                        public static class Outer {
                            private Inner inner = new Inner();
                            public Inner getInner() { return inner; }
                        }
                        public void run() {
                            Outer outer = new Outer();
                            String value = outer.getInner().code();
                        }
                    }
                    """);

            assertResolvedValue(tempDir, "value", "INNER");
        }

        @Test
        void shouldResolveSuperclassFieldViaSubclassThisGetter(@TempDir Path tempDir) throws IOException {
            writeJava(tempDir, "com/example/Runner.java", """
                    package com.example;
                    public class Runner {
                        public static class Base {
                            protected String event = "SUPER";
                        }
                        public static class Sub extends Base {
                            public String getEvent() { return event; }
                        }
                        public void run() {
                            Sub sub = new Sub();
                            String value = sub.getEvent();
                        }
                    }
                    """);

            assertResolvedValue(tempDir, "value", "SUPER");
        }

        @Test
        void shouldResolveInterfaceEnumGetterViaConcreteImplementation(@TempDir Path tempDir) throws IOException {
            writeJava(tempDir, "com/example/Runner.java", """
                    package com.example;
                    public class Runner {
                        public interface RichEvent {
                            EventType getType();
                        }
                        public static class PayEvent implements RichEvent {
                            public EventType getType() { return EventType.PAY; }
                        }
                        public void run() {
                            RichEvent event = new PayEvent();
                            EventType value = event.getType();
                        }
                    }
                    enum EventType { PAY, SHIP }
                    """);

            assertResolvedValue(tempDir, "value", "EventType.PAY");
        }

        @Test
        void shouldReturnEmptyWhenAccessorIndexHasNoEntry(@TempDir Path tempDir) throws IOException {
            writeJava(tempDir, "com/example/Runner.java", """
                    package com.example;
                    public class Runner {
                        public static class Worker {
                            public String compute() { return "WORK"; }
                        }
                        public void run() {
                            Worker worker = new Worker();
                            String value = worker.compute();
                        }
                    }
                    """);

            assertResolvedValue(tempDir, "value", "WORK");
            assertThat(accessorLookup(tempDir, "com.example.Runner.Worker", "compute")).isEmpty();
        }
    }

    private static void assertResolvedValue(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 void assertAccessorIndexed(Path tempDir, String ownerFqn, String methodName) throws IOException {
        assertThat(accessorLookup(tempDir, ownerFqn, methodName)).isPresent();
    }

    private static java.util.Optional<AccessorSummary> accessorLookup(Path tempDir, String ownerFqn, String methodName)
            throws IOException {
        CodebaseContext context = scan(tempDir);
        return context.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()) {
            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 nestedResult = findInitializerInType(nested, methodName, variableName);
                if (nestedResult != null) {
                    return nestedResult;
                }
            }
        }
        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);
    }
}
