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.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.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;

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

class AccessorPipelineBugFixTest {

    @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 scanWorkspace() throws IOException {
        context.scan(tempDir);
    }

    @Nested
    class ConstructorAnalyzerSubclassPollution {

        @Test
        void shouldNotPullSubclassFieldValuesWhenTracingConcreteBaseType() throws IOException {
            writeJava("com/example/Base.java", """
                    package com.example;
                    public class Base {
                        protected String token;
                    }
                    """);
            writeJava("com/example/Sub.java", """
                    package com.example;
                    public class Sub extends Base {
                        public Sub() { this.token = "SUB_ONLY"; }
                    }
                    """);
            scanWorkspace();

            TypeDeclaration baseTd = context.getTypeDeclaration("com.example.Base");
            List<String> traced = constructorAnalyzer.traceFieldInConstructors(
                    baseTd, "token", context, new HashSet<>(), ResolutionBudget.defaults());

            assertThat(traced).doesNotContain("SUB_ONLY");
        }
    }

    @Nested
    class ConstantExtractorAbstractBodyWidening {

        @Test
        void shouldWidenToImplementationsWhenInterfaceMethodHasNoBody() throws IOException {
            writeJava("com/example/Code.java", """
                    package com.example;
                    public enum Code { A, B }
                    """);
            writeJava("com/example/RichEvent.java", """
                    package com.example;
                    public interface RichEvent {
                        Code getCode();
                    }
                    """);
            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; }
                    }
                    """);
            scanWorkspace();

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

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

        @Test
        void shouldWidenToImplsEvenWhenAbstractFieldHasInlineInitializer() throws IOException {
            writeJava("com/example/Code.java", """
                    package com.example;
                    public enum Code { PAY, SHIP }
                    """);
            writeJava("com/example/BaseEvent.java", """
                    package com.example;
                    public abstract class BaseEvent {
                        protected Code code = Code.PAY;
                        public Code getCode() { return code; }
                    }
                    """);
            writeJava("com/example/DefaultEvent.java", """
                    package com.example;
                    public class DefaultEvent extends BaseEvent {}
                    """);
            writeJava("com/example/ShipEvent.java", """
                    package com.example;
                    public class ShipEvent extends BaseEvent {
                        public ShipEvent() { this.code = Code.SHIP; }
                    }
                    """);
            scanWorkspace();

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

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

    @Nested
    class AccessorResolverCacheKey {

        @Test
        void shouldNotReuseCachedResultWhenFieldValuesDiffer() throws IOException {
            writeJava("com/example/Payload.java", """
                    package com.example;
                    public class Payload {
                        private String event;
                        public String getEvent() { return event; }
                    }
                    """);
            scanWorkspace();

            TypeDeclaration td = context.getTypeDeclaration("com.example.Payload");
            Map<String, String> valuesA = new HashMap<>(Map.of("event", "PAY"));
            Map<String, String> valuesB = new HashMap<>(Map.of("event", "CANCEL"));

            List<String> pay = accessorResolver.resolve(
                    "com.example.Payload",
                    "getEvent",
                    new AccessorResolver.ReceiverContext(td, null, null, false, valuesA),
                    ResolutionBudget.defaults());
            List<String> cancel = accessorResolver.resolve(
                    "com.example.Payload",
                    "getEvent",
                    new AccessorResolver.ReceiverContext(td, null, null, false, valuesB),
                    ResolutionBudget.defaults());

            assertThat(pay).containsExactly("PAY");
            assertThat(cancel).containsExactly("CANCEL");
        }
    }

    @Nested
    class InlineAccessorsDisabled {

        @Test
        void shouldSkipIndexedDataflowShortcutWhenDisabled() throws IOException {
            writeJava("com/example/Payload.java", """
                    package com.example;
                    public class Payload {
                        private String event = "INLINE";
                        public String getEvent() { return event; }
                    }
                    """);
            context.setInlineAccessors(false);
            scanWorkspace();

            assertThat(context.getAccessorIndex().trivialCount()).isZero();
            assertThat(context.isInlineAccessors()).isFalse();
        }
    }
}
