package click.kamil.springstatemachineexporter.analysis.pipeline;

import click.kamil.springstatemachineexporter.analysis.model.CallChain;
import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
import click.kamil.springstatemachineexporter.analysis.resolver.ConstantResolver;
import click.kamil.springstatemachineexporter.analysis.service.ConstructorAnalyzer;
import click.kamil.springstatemachineexporter.analysis.service.ConstantExtractor;
import click.kamil.springstatemachineexporter.analysis.service.HeuristicCallGraphEngine;
import click.kamil.springstatemachineexporter.analysis.service.VariableTracer;
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.MethodDeclaration;
import org.eclipse.jdt.core.dom.MethodInvocation;
import org.eclipse.jdt.core.dom.TypeDeclaration;
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
import org.junit.jupiter.api.BeforeEach;
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.HashSet;
import java.util.List;

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

class InheritanceAndNestedResolutionTest {

    @TempDir
    Path tempDir;

    CodebaseContext context;
    ConstantExtractor constantExtractor;
    ConstructorAnalyzer constructorAnalyzer;
    AccessorResolver accessorResolver;
    VariableTracer variableTracer;

    @BeforeEach
    void setUp() throws IOException {
        context = new CodebaseContext();
        context.setResolveBindings(true);
        context.setSourcepath(List.of(tempDir.toAbsolutePath().toString()));
        ConstantResolver constantResolver = context.getConstantResolver();
        constantExtractor = new ConstantExtractor(context, constantResolver, mi -> null);
        constructorAnalyzer = new ConstructorAnalyzer(context, constantResolver);
        variableTracer = new VariableTracer(context, constantResolver);
        constantExtractor.setVariableTracer(variableTracer);
        constantExtractor.setConstructorAnalyzer(constructorAnalyzer);
        constructorAnalyzer.setVariableTracer(variableTracer);
        constructorAnalyzer.setConstantExtractor(constantExtractor);
        accessorResolver = new AccessorResolver(context, constantExtractor, constructorAnalyzer, constantResolver);
        constantExtractor.setAccessorResolver(accessorResolver);
    }

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

    private void scan() throws IOException {
        context.scan(tempDir);
    }

    @Nested
    class InterfaceDefaultMethod {

        @Test
        void accessorResolverShouldIncludeDefaultMethodAndAllImpls() throws IOException {
            writeJava("com/example/Code.java", """
                    package com.example;
                    public enum Code { DEFAULT, PAY, SHIP }
                    """);
            writeJava("com/example/EventSource.java", """
                    package com.example;
                    public interface EventSource {
                        default Code getCode() { return Code.DEFAULT; }
                    }
                    """);
            writeJava("com/example/PayEvent.java", """
                    package com.example;
                    public class PayEvent implements EventSource {
                        public Code getCode() { return Code.PAY; }
                    }
                    """);
            writeJava("com/example/ShipEvent.java", """
                    package com.example;
                    public class ShipEvent implements EventSource {
                        public Code getCode() { return Code.SHIP; }
                    }
                    """);
            scan();

            List<String> resolved = accessorResolver.resolve(
                    "com.example.EventSource",
                    "getCode",
                    new AccessorResolver.ReceiverContext(
                            context.getTypeDeclaration("com.example.EventSource"),
                            null, null, false, null),
                    ResolutionBudget.defaults());

            assertThat(resolved).containsExactlyInAnyOrder("Code.DEFAULT", "Code.PAY", "Code.SHIP");
        }

        @Test
        void accessorResolverShouldMergeDefaultMethodWithConcreteOverrides() throws IOException {
            writeJava("com/example/EventSource.java", """
                    package com.example;
                    public interface EventSource {
                        default String getCode() { return "DEFAULT"; }
                    }
                    """);
            writeJava("com/example/PayEvent.java", """
                    package com.example;
                    public class PayEvent implements EventSource {
                        public String getCode() { return "PAY"; }
                    }
                    """);
            writeJava("com/example/ShipEvent.java", """
                    package com.example;
                    public class ShipEvent implements EventSource {
                        public String getCode() { return "SHIP"; }
                    }
                    """);
            scan();

            List<String> resolved = accessorResolver.resolve(
                    "com.example.EventSource",
                    "getCode",
                    new AccessorResolver.ReceiverContext(
                            context.getTypeDeclaration("com.example.EventSource"),
                            null, null, false, null),
                    ResolutionBudget.defaults());

            assertThat(resolved).containsExactlyInAnyOrder("DEFAULT", "PAY", "SHIP");
        }

        @Test
        void accessorResolverShouldPreferConcreteCicOverInterfaceDefault() throws IOException {
            writeJava("com/example/Code.java", """
                    package com.example;
                    public enum Code { DEFAULT, PAY, SHIP }
                    """);
            writeJava("com/example/EventSource.java", """
                    package com.example;
                    public interface EventSource {
                        default Code getCode() { return Code.DEFAULT; }
                    }
                    """);
            writeJava("com/example/PayEvent.java", """
                    package com.example;
                    public class PayEvent implements EventSource {
                        public Code getCode() { return Code.PAY; }
                    }
                    """);
            writeJava("com/example/Runner.java", """
                    package com.example;
                    public class Runner {
                        void run() {
                            EventSource src = new PayEvent();
                        }
                    }
                    """);
            scan();

            org.eclipse.jdt.core.dom.TypeDeclaration runner = context.getTypeDeclaration("com.example.Runner");
            org.eclipse.jdt.core.dom.CompilationUnit cu = (org.eclipse.jdt.core.dom.CompilationUnit) runner.getRoot();
            final org.eclipse.jdt.core.dom.ClassInstanceCreation[] payEventCic =
                    new org.eclipse.jdt.core.dom.ClassInstanceCreation[1];
            cu.accept(new org.eclipse.jdt.core.dom.ASTVisitor() {
                @Override
                public boolean visit(org.eclipse.jdt.core.dom.ClassInstanceCreation node) {
                    if ("PayEvent".equals(node.getType().toString())) {
                        payEventCic[0] = node;
                    }
                    return super.visit(node);
                }
            });

            List<String> resolved = accessorResolver.resolve(
                    "com.example.EventSource",
                    "getCode",
                    new AccessorResolver.ReceiverContext(
                            context.getTypeDeclaration("com.example.EventSource"),
                            payEventCic[0],
                            cu,
                            false,
                            null),
                    ResolutionBudget.defaults());

            assertThat(resolved).containsExactly("Code.PAY");
        }
    }

    @Nested
    class NonIndexedConcreteOverride {

        @Test
        void dataFlowShouldResolveNonTrivialConcreteOverride() throws IOException {
            writeJava("com/example/Runner.java", """
                    package com.example;
                    public class Runner {
                        public void run() {
                            Payload payload = new CustomPayload();
                            String event = payload.getEvent();
                        }
                    }
                    interface Payload {
                        String getEvent();
                    }
                    class CustomPayload implements Payload {
                        public String getEvent() {
                            return sideEffect();
                        }
                        private String sideEffect() { return "CUSTOM"; }
                    }
                    """);
            scan();

            TypeDeclaration runner = context.getTypeDeclaration("com.example.Runner");
            MethodDeclaration run = context.findMethodDeclaration(runner, "run", true);
            final MethodInvocation[] getterCall = new MethodInvocation[1];
            run.getBody().accept(new org.eclipse.jdt.core.dom.ASTVisitor() {
                @Override
                public boolean visit(VariableDeclarationFragment node) {
                    if (node.getInitializer() instanceof MethodInvocation mi) {
                        getterCall[0] = mi;
                    }
                    return super.visit(node);
                }
            });

            DataFlowModel model = new JdtDataFlowModel(context);
            String resolved = model.resolveValue(getterCall[0], context);
            assertThat(resolved).isEqualTo("CUSTOM");
        }
    }

    @Nested
    class GetterChainThroughInterface {

        @Test
        void callGraphShouldResolveDeepChainWithInterfaceMiddleHop() throws IOException {
            writeJava("com/example/Api.java", """
                    package com.example;
                    public class ApiController {
                        CentralDispatcher dispatcher;
                        Outer outer;
                        public void pay() {
                            dispatcher.fire(outer.getInner().getPayload().getEvent());
                        }
                    }
                    class CentralDispatcher {
                        public void fire(OrderEvent event) {}
                    }
                    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 }
                    class OrderStateMachineConfig {}
                    """);
            scan();

            HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
            EntryPoint entry = EntryPoint.builder()
                    .className("com.example.ApiController")
                    .methodName("pay")
                    .build();
            TriggerPoint trigger = TriggerPoint.builder()
                    .className("com.example.CentralDispatcher")
                    .methodName("fire")
                    .event("event")
                    .build();

            List<CallChain> chains = engine.findChains(List.of(entry), List.of(trigger));
            assertThat(chains).hasSize(1);
            assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents())
                    .contains("OrderEvent.PAY");
        }
    }

    @Nested
    class ConcreteSubtypeTrigger {

        @Test
        void callGraphShouldPreferConcreteSubtypeFieldOverAbstractDefault() throws IOException {
            writeJava("com/example/Api.java", """
                    package com.example;
                    public class ApiController {
                        OrderService service;
                        public void ship() {
                            service.fire(new ShipEvent());
                        }
                    }
                    class OrderService {
                        StateMachine sm;
                        public void fire(BaseEvent event) {
                            sm.sendEvent(event.getCode());
                        }
                    }
                    enum Code { PAY, SHIP }
                    abstract class BaseEvent {
                        protected Code code = Code.PAY;
                        public Code getCode() { return code; }
                    }
                    class ShipEvent extends BaseEvent {
                        public ShipEvent() { this.code = Code.SHIP; }
                    }
                    class StateMachine { void sendEvent(Code event) {} }
                    class OrderStateMachineConfig {}
                    """);
            scan();

            HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
            EntryPoint entry = EntryPoint.builder()
                    .className("com.example.ApiController")
                    .methodName("ship")
                    .build();
            TriggerPoint trigger = TriggerPoint.builder()
                    .className("com.example.StateMachine")
                    .methodName("sendEvent")
                    .event("event")
                    .build();

            List<CallChain> chains = engine.findChains(List.of(entry), List.of(trigger));
            assertThat(chains).hasSize(1);
            assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents())
                    .containsExactly("Code.SHIP");
        }
    }

    @Nested
    class ConcreteSubtypeTriggerViaFire {

        @Test
        void callGraphShouldResolveSubtypeWhenPassedToBaseTypedFire() throws IOException {
            writeJava("com/example/Api.java", """
                    package com.example;
                    public class ApiController {
                        OrderService service;
                        public void ship() {
                            service.fire(new ShipEvent());
                        }
                    }
                    class OrderService {
                        public void fire(BaseEvent event) {}
                    }
                    enum Code { PAY, SHIP }
                    abstract class BaseEvent {
                        protected Code code = Code.PAY;
                        public Code getCode() { return code; }
                    }
                    class ShipEvent extends BaseEvent {
                        public ShipEvent() { this.code = Code.SHIP; }
                    }
                    class OrderStateMachineConfig {}
                    """);
            scan();

            HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
            EntryPoint entry = EntryPoint.builder()
                    .className("com.example.ApiController")
                    .methodName("ship")
                    .build();
            TriggerPoint trigger = TriggerPoint.builder()
                    .className("com.example.OrderService")
                    .methodName("fire")
                    .event("event")
                    .build();

            List<CallChain> chains = engine.findChains(List.of(entry), List.of(trigger));
            assertThat(chains).hasSize(1);
            assertThat(chains.get(0).getMethodChain())
                    .containsExactly(
                            "com.example.ApiController.ship",
                            "com.example.OrderService.fire");
            assertThat(chains.get(0).getTriggerPoint().getEvent())
                    .contains("ShipEvent");
        }
    }

    @Nested
    class UnresolvedTypeWidening {

        @Test
        void constantExtractorShouldWidenWhenTypeDeclarationMissing() throws IOException {
            writeJava("com/example/Code.java", """
                    package com.example;
                    public enum Code { A, B }
                    """);
            writeJava("com/example/EventA.java", """
                    package com.example;
                    public class EventA implements RichEvent {
                        public Code getCode() { return Code.A; }
                    }
                    """);
            writeJava("com/example/EventB.java", """
                    package com.example;
                    public class EventB implements RichEvent {
                        public Code getCode() { return Code.B; }
                    }
                    """);
            writeJava("com/example/RichEvent.java", """
                    package com.example;
                    public interface RichEvent {
                        Code getCode();
                    }
                    """);
            scan();

            List<String> resolved = constantExtractor.resolveMethodReturnConstant(
                    "RichEvent", "getCode", 0, new HashSet<>(), null);

            assertThat(resolved).containsExactlyInAnyOrder("Code.A", "Code.B");
        }
    }

    @Nested
    class DeterministicImplementations {

        @Test
        void getImplementationsShouldReturnSortedStableOrder() throws IOException {
            writeJava("com/example/EventSource.java", """
                    package com.example;
                    public interface EventSource {
                        String getCode();
                    }
                    """);
            writeJava("com/example/ZebraEvent.java", """
                    package com.example;
                    public class ZebraEvent implements EventSource {
                        public String getCode() { return "Z"; }
                    }
                    """);
            writeJava("com/example/AlphaEvent.java", """
                    package com.example;
                    public class AlphaEvent implements EventSource {
                        public String getCode() { return "A"; }
                    }
                    """);
            scan();

            List<String> first = context.getImplementations("com.example.EventSource");
            List<String> second = context.getImplementations("com.example.EventSource");

            assertThat(first).containsExactly(
                    "com.example.AlphaEvent",
                    "com.example.ZebraEvent");
            assertThat(second).isEqualTo(first);
        }
    }

    @Nested
    class ReactiveFlatMapLambdaSendEvent {

        @Test
        void callGraphShouldResolveGetterInsideFlatMapLambda() throws IOException {
            writeJava("com/example/Api.java", """
                    package com.example;
                    public class ApiController {
                        StateMachine sm;
                        Payload payload;
                        public void handle() {
                            Mono.just(payload).flatMap(p -> sm.sendEvent(p.getEvent()));
                        }
                    }
                    class Payload {
                        OrderEvent event = OrderEvent.PAY;
                        OrderEvent getEvent() { return event; }
                    }
                    enum OrderEvent { PAY, SHIP }
                    class StateMachine { void sendEvent(OrderEvent event) {} }
                    class Mono {
                        static Mono just(Object o) { return new Mono(); }
                        Mono flatMap(Object fn) { return this; }
                    }
                    class OrderStateMachineConfig {}
                    """);
            scan();

            HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
            EntryPoint entry = EntryPoint.builder()
                    .className("com.example.ApiController")
                    .methodName("handle")
                    .build();
            TriggerPoint trigger = TriggerPoint.builder()
                    .className("com.example.StateMachine")
                    .methodName("sendEvent")
                    .event("event")
                    .build();

            List<CallChain> chains = engine.findChains(List.of(entry), List.of(trigger));
            assertThat(chains).hasSize(1);
            assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents())
                    .containsExactly("OrderEvent.PAY");
        }

        @Test
        void callGraphShouldResolveGetterInsideFlatMapBlockBody() throws IOException {
            writeJava("com/example/Api.java", """
                    package com.example;
                    public class ApiController {
                        StateMachine sm;
                        Payload payload;
                        public void handle() {
                            Mono.just(payload).flatMap(p -> {
                                sm.sendEvent(p.getEvent());
                                return Mono.empty();
                            });
                        }
                    }
                    class Payload {
                        OrderEvent event = OrderEvent.PAY;
                        OrderEvent getEvent() { return event; }
                    }
                    enum OrderEvent { PAY, SHIP }
                    class StateMachine { void sendEvent(OrderEvent event) {} }
                    class Mono {
                        static Mono just(Object o) { return new Mono(); }
                        Mono flatMap(Object fn) { return this; }
                        static Mono empty() { return new Mono(); }
                    }
                    class OrderStateMachineConfig {}
                    """);
            scan();

            HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
            EntryPoint entry = EntryPoint.builder()
                    .className("com.example.ApiController")
                    .methodName("handle")
                    .build();
            TriggerPoint trigger = TriggerPoint.builder()
                    .className("com.example.StateMachine")
                    .methodName("sendEvent")
                    .event("event")
                    .build();

            List<CallChain> chains = engine.findChains(List.of(entry), List.of(trigger));
            assertThat(chains).hasSize(1);
            assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents())
                    .containsExactly("OrderEvent.PAY");
        }

        @Test
        void callGraphShouldResolveGetterThroughNestedFlatMap() throws IOException {
            writeJava("com/example/Api.java", """
                    package com.example;
                    public class ApiController {
                        StateMachine sm;
                        Payload payload;
                        public void handle() {
                            Mono.just(payload)
                                    .flatMap(x -> Mono.just(x.getInner()))
                                    .flatMap(p -> sm.sendEvent(p.getEvent()));
                        }
                    }
                    class Payload {
                        Inner inner = new Inner();
                        Inner getInner() { return inner; }
                    }
                    class Inner {
                        OrderEvent event = OrderEvent.SHIP;
                        OrderEvent getEvent() { return event; }
                    }
                    enum OrderEvent { PAY, SHIP }
                    class StateMachine { void sendEvent(OrderEvent event) {} }
                    class Mono {
                        static Mono just(Object o) { return new Mono(); }
                        Mono flatMap(Object fn) { return this; }
                    }
                    class OrderStateMachineConfig {}
                    """);
            scan();

            HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
            EntryPoint entry = EntryPoint.builder()
                    .className("com.example.ApiController")
                    .methodName("handle")
                    .build();
            TriggerPoint trigger = TriggerPoint.builder()
                    .className("com.example.StateMachine")
                    .methodName("sendEvent")
                    .event("event")
                    .build();

            List<CallChain> chains = engine.findChains(List.of(entry), List.of(trigger));
            assertThat(chains).hasSize(1);
            assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents())
                    .containsExactly("OrderEvent.SHIP");
        }

        @Test
        void callGraphShouldResolveGetterInsideMapLambda() throws IOException {
            writeJava("com/example/Api.java", """
                    package com.example;
                    public class ApiController {
                        StateMachine sm;
                        Payload payload;
                        public void handle() {
                            Mono.just(payload).map(p -> sm.sendEvent(p.getEvent()));
                        }
                    }
                    class Payload {
                        OrderEvent event = OrderEvent.PAY;
                        OrderEvent getEvent() { return event; }
                    }
                    enum OrderEvent { PAY, SHIP }
                    class StateMachine { void sendEvent(OrderEvent event) {} }
                    class Mono {
                        static Mono just(Object o) { return new Mono(); }
                        Mono map(Object fn) { return this; }
                    }
                    class OrderStateMachineConfig {}
                    """);
            scan();

            HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
            EntryPoint entry = EntryPoint.builder()
                    .className("com.example.ApiController")
                    .methodName("handle")
                    .build();
            TriggerPoint trigger = TriggerPoint.builder()
                    .className("com.example.StateMachine")
                    .methodName("sendEvent")
                    .event("event")
                    .build();

            List<CallChain> chains = engine.findChains(List.of(entry), List.of(trigger));
            assertThat(chains).hasSize(1);
            assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents())
                    .containsExactly("OrderEvent.PAY");
        }

        @Test
        void callGraphShouldResolveGetterThroughNestedSwitchMap() throws IOException {
            writeJava("com/example/Api.java", """
                    package com.example;
                    public class ApiController {
                        StateMachine sm;
                        Payload payload;
                        public void handle() {
                            Mono.just(payload)
                                    .switchMap(x -> Mono.just(x.getInner()))
                                    .switchMap(p -> sm.sendEvent(p.getEvent()));
                        }
                    }
                    class Payload {
                        Inner inner = new Inner();
                        Inner getInner() { return inner; }
                    }
                    class Inner {
                        OrderEvent event = OrderEvent.SHIP;
                        OrderEvent getEvent() { return event; }
                    }
                    enum OrderEvent { PAY, SHIP }
                    class StateMachine { void sendEvent(OrderEvent event) {} }
                    class Mono {
                        static Mono just(Object o) { return new Mono(); }
                        Mono switchMap(Object fn) { return this; }
                    }
                    class OrderStateMachineConfig {}
                    """);
            scan();

            HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
            EntryPoint entry = EntryPoint.builder()
                    .className("com.example.ApiController")
                    .methodName("handle")
                    .build();
            TriggerPoint trigger = TriggerPoint.builder()
                    .className("com.example.StateMachine")
                    .methodName("sendEvent")
                    .event("event")
                    .build();

            List<CallChain> chains = engine.findChains(List.of(entry), List.of(trigger));
            assertThat(chains).hasSize(1);
            assertThat(chains.get(0).getTriggerPoint().getPolymorphicEvents())
                    .containsExactly("OrderEvent.SHIP");
        }
    }
}
