package click.kamil.springstatemachineexporter.analysis.pipeline;

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.ExpressionAccessClassifier;
import click.kamil.springstatemachineexporter.analysis.service.HeuristicCallGraphEngine;
import click.kamil.springstatemachineexporter.analysis.service.VariableTracer;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import org.eclipse.jdt.core.dom.TypeDeclaration;
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 PipelineRefactorTest {

    @TempDir
    Path tempDir;

    CodebaseContext context;
    ConstantExtractor constantExtractor;
    ConstructorAnalyzer constructorAnalyzer;
    AccessorResolver accessorResolver;
    ExpressionAccessClassifier expressionAccessClassifier;

    @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 variableTracer = new VariableTracer(context, constantResolver);
        constantExtractor.setVariableTracer(variableTracer);
        constantExtractor.setConstructorAnalyzer(constructorAnalyzer);
        constructorAnalyzer.setVariableTracer(variableTracer);
        constructorAnalyzer.setConstantExtractor(constantExtractor);
        expressionAccessClassifier = new ExpressionAccessClassifier(context, variableTracer, constantResolver, expr -> null);
        constantExtractor.setExpressionAccessClassifier(expressionAccessClassifier);
        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 scanWorkspace() throws IOException {
        context.scan(tempDir);
    }

    @Nested
    class ResolutionBudgetTests {

        @Test
        void shouldUseSharedDefaults() {
            ResolutionBudget budget = ResolutionBudget.defaults();
            assertThat(budget.isMethodReturnExhausted(20)).isFalse();
            assertThat(budget.isMethodReturnExhausted(21)).isTrue();
            assertThat(budget.isUnwrapExhausted(5)).isFalse();
            assertThat(budget.isUnwrapExhausted(6)).isTrue();
            assertThat(budget.allowGlobalFieldScan()).isFalse();
        }

        @Test
        void shouldDecrementDepthLimitsViaChild() {
            ResolutionBudget child = ResolutionBudget.defaults().child();
            assertThat(child.isMethodReturnExhausted(19)).isFalse();
            assertThat(child.isMethodReturnExhausted(20)).isTrue();
            assertThat(child.isDataflowExhausted(49)).isFalse();
            assertThat(child.isDataflowExhausted(50)).isTrue();
        }
    }

    @Nested
    class LibraryUnwrapRegistryTests {

        @Test
        void shouldRecognizeReactiveReceiversAndFactoryMethods() {
            assertThat(LibraryUnwrapRegistry.isUnwrapReceiverType("Mono")).isTrue();
            assertThat(LibraryUnwrapRegistry.isUnwrapMethodName("just")).isTrue();
            assertThat(LibraryUnwrapRegistry.isReactiveFactoryMethod("withPayload")).isTrue();
            assertThat(LibraryUnwrapRegistry.isReactiveTransformMethod("flatMap")).isTrue();
            assertThat(LibraryUnwrapRegistry.shouldStopUnwrap("buildPayload")).isTrue();
            assertThat(LibraryUnwrapRegistry.isEventSetterMethodName("eventType")).isTrue();
        }
    }

    @Nested
    class MethodInvocationUnwrapperTests {

        @Test
        void shouldPeelOptionalOfFactoryCall() throws IOException {
            writeJava("com/example/Factory.java", """
                    package com.example;
                    public class Factory {
                        public static String build() {
                            return Optional.of("PAY").get();
                        }
                    }
                    """);
            scanWorkspace();
            TypeDeclaration td = context.getTypeDeclaration("com.example.Factory");
            assertThat(td).isNotNull();
        }
    }

    @Nested
    class AccessorResolverTests {

        @Test
        void shouldResolveConstantGetterWithoutCic() throws IOException {
            writeJava("com/example/Events.java", """
                    package com.example;
                    public enum Events { PAY, CANCEL }
                    """);
            writeJava("com/example/EventHolder.java", """
                    package com.example;
                    public class EventHolder {
                        public Events getEvent() { return Events.PAY; }
                    }
                    """);
            scanWorkspace();
            List<String> resolved = accessorResolver.resolve(
                    "com.example.EventHolder",
                    "getEvent",
                    new AccessorResolver.ReceiverContext(null, null, null, false, null),
                    ResolutionBudget.defaults());
            assertThat(resolved).contains("Events.PAY");
        }

        @Test
        void shouldCacheRepeatedLookups() throws IOException {
            writeJava("com/example/Events.java", """
                    package com.example;
                    public enum Events { PAY }
                    """);
            writeJava("com/example/EventHolder.java", """
                    package com.example;
                    public class EventHolder {
                        public Events getEvent() { return Events.PAY; }
                    }
                    """);
            scanWorkspace();
            AccessorResolver.ReceiverContext receiverContext =
                    new AccessorResolver.ReceiverContext(null, null, null, false, null);
            List<String> first = accessorResolver.resolve(
                    "com.example.EventHolder", "getEvent", receiverContext, ResolutionBudget.defaults());
            List<String> second = accessorResolver.resolve(
                    "com.example.EventHolder", "getEvent", receiverContext, ResolutionBudget.defaults());
            assertThat(first).contains("Events.PAY");
            assertThat(second).isSameAs(first);
        }

        @Test
        void shouldResolveGetterChainStepViaIndexBeforeReturnedCic() throws IOException {
            writeJava("com/example/Payload.java", """
                    package com.example;
                    public class Payload {
                        public String getType() { return "PAY"; }
                    }
                    """);
            writeJava("com/example/Inner.java", """
                    package com.example;
                    public class Inner {
                        private final Payload payload = new Payload();
                        public Payload getPayload() { return payload; }
                    }
                    """);
            scanWorkspace();
            AccessorResolver.GetterChainStep step = accessorResolver.resolveGetterChainStep(
                    "com.example.Inner", "getPayload", ResolutionBudget.defaults());
            assertThat(step).isNotNull();
            assertThat(step.typeFqn()).contains("Payload");
            assertThat(step.cic()).isNotNull();
        }

        @Test
        void shouldResolveFactoryBuildThenGetterWithoutCicDeadEnd() throws IOException {
            writeJava("com/example/Events.java", """
                    package com.example;
                    public enum Events { PAY }
                    """);
            writeJava("com/example/Order.java", """
                    package com.example;
                    public class Order {
                        public Events getEvent() { return Events.PAY; }
                    }
                    """);
            writeJava("com/example/OrderFactory.java", """
                    package com.example;
                    public class OrderFactory {
                        public Order build() { return new Order(); }
                    }
                    """);
            writeJava("com/example/Runner.java", """
                    package com.example;
                    public class Runner {
                        public void run() {
                            OrderFactory factory = new OrderFactory();
                            Order order = factory.build();
                            Events event = order.getEvent();
                        }
                    }
                    """);
            scanWorkspace();
            List<String> events = accessorResolver.resolve(
                    "com.example.Order",
                    "getEvent",
                    new AccessorResolver.ReceiverContext(
                            context.getTypeDeclaration("com.example.Order"),
                            null,
                            null,
                            false,
                            null),
                    ResolutionBudget.defaults());
            assertThat(events).contains("Events.PAY");
        }

        @Test
        void shouldWidenInterfaceGettersToAllImplementations() throws IOException {
            writeJava("com/example/Events.java", """
                    package com.example;
                    public enum Events { ALPHA, BETA }
                    """);
            writeJava("com/example/AlphaPayload.java", """
                    package com.example;
                    public class AlphaPayload implements Payload {
                        public Events type() { return Events.ALPHA; }
                    }
                    """);
            writeJava("com/example/BetaPayload.java", """
                    package com.example;
                    public class BetaPayload implements Payload {
                        public Events type() { return Events.BETA; }
                    }
                    """);
            writeJava("com/example/Payload.java", """
                    package com.example;
                    public interface Payload {
                        Events type();
                    }
                    """);
            scanWorkspace();
            List<String> resolved = accessorResolver.resolve(
                    "com.example.Payload",
                    "type",
                    new AccessorResolver.ReceiverContext(
                            context.getTypeDeclaration("com.example.Payload"),
                            null,
                            null,
                            false,
                            null),
                    ResolutionBudget.defaults());
            assertThat(resolved).containsExactlyInAnyOrder("Events.ALPHA", "Events.BETA");
        }
    }

    @Nested
    class ExpressionAccessClassifierSuffixTests {

        @Test
        void shouldTreatRecordStyleSuffixesAsTraceable() {
            assertThat(expressionAccessClassifier.isTraceableAccessorSuffix(".getEvent()")).isTrue();
            assertThat(expressionAccessClassifier.isTraceableAccessorSuffix(".type()")).isTrue();
            assertThat(expressionAccessClassifier.isTraceableAccessorSuffix(".event()")).isTrue();
            assertThat(expressionAccessClassifier.isTraceableAccessorSuffix(".hashCode()")).isFalse();
        }
    }

    @Nested
    class ConstructorAnalyzerGlobalScanTests {

        @Test
        void shouldSkipGlobalScanByDefault() throws IOException {
            writeJava("com/example/Unrelated.java", """
                    package com.example;
                    public class Unrelated {
                        private String sharedName = "SHOULD_NOT_MATCH";
                    }
                    """);
            writeJava("com/example/Target.java", """
                    package com.example;
                    public class Target {
                        private String sharedName;
                        public Target() { this.sharedName = "LOCAL"; }
                    }
                    """);
            scanWorkspace();
            TypeDeclaration td = context.getTypeDeclaration("com.example.Target");
            List<String> traced = constructorAnalyzer.traceFieldInConstructors(
                    td, "sharedName", context, new HashSet<>(), ResolutionBudget.defaults());
            assertThat(traced).contains("LOCAL");
            assertThat(traced).doesNotContain("SHOULD_NOT_MATCH");
        }

        @Test
        void shouldAllowGlobalScanWhenExplicitlyEnabled() throws IOException {
            writeJava("com/example/OnlyFieldOwner.java", """
                    package com.example;
                    public class OnlyFieldOwner {
                        private String orphanField = "GLOBAL";
                    }
                    """);
            writeJava("com/example/Shell.java", """
                    package com.example;
                    public class Shell {
                        private String orphanField;
                    }
                    """);
            scanWorkspace();
            TypeDeclaration shell = context.getTypeDeclaration("com.example.Shell");
            List<String> withoutGlobal = constructorAnalyzer.traceFieldInConstructors(
                    shell, "orphanField", context, new HashSet<>(), ResolutionBudget.defaults());
            assertThat(withoutGlobal).isEmpty();

            List<String> withGlobal = constructorAnalyzer.traceFieldInConstructors(
                    shell, "orphanField", context, new HashSet<>(), ResolutionBudget.withGlobalFieldScan());
            assertThat(withGlobal).contains("GLOBAL");
        }
    }

    @Nested
    class ConstantResolverSharingTests {

        @Test
        void shouldUseSingleConstantResolverFromContext() {
            assertThat(new HeuristicCallGraphEngine(context).getClass()).isNotNull();
            ConstantResolver shared = context.getConstantResolver();
            ConstantExtractor extractor = new ConstantExtractor(context, shared, mi -> null);
            assertThat(extractor).isNotNull();
            assertThat(shared).isSameAs(context.getConstantResolver());
        }
    }
}
