package click.kamil.springstatemachineexporter.analysis.index;

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.ast.common.CodebaseContext;
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 PolymorphicAccessorIndexTest {

    @TempDir
    Path tempDir;

    CodebaseContext context;
    ConstantExtractor constantExtractor;

    @BeforeEach
    void setUp() {
        context = new CodebaseContext();
        constantExtractor = new ConstantExtractor(
                context,
                new ConstantResolver(),
                mi -> null);
        constantExtractor.setConstructorAnalyzer(new ConstructorAnalyzer(context, context.getConstantResolver()));
    }

    @Test
    void shouldPrecomputeWidenedConstantsForThreeEventImplementations() throws IOException {
        writeJava("com/example/EventSource.java", """
                package com.example;
                public interface EventSource {
                    String getEvent();
                }
                """);
        writeJava("com/example/PayEvent.java", """
                package com.example;
                public class PayEvent implements EventSource {
                    public String getEvent() { return "PAY"; }
                }
                """);
        writeJava("com/example/ShipEvent.java", """
                package com.example;
                public class ShipEvent implements EventSource {
                    public String getEvent() { return "SHIP"; }
                }
                """);
        writeJava("com/example/CancelEvent.java", """
                package com.example;
                public class CancelEvent implements EventSource {
                    public String getEvent() { return "CANCEL"; }
                }
                """);
        scan();

        PolymorphicAccessorIndex index = context.getPolymorphicAccessorIndex();
        assertThat(index.entryCount()).isGreaterThan(0);

        List<String> precomputed = index.widenedReturnConstants("com.example.EventSource", "getEvent")
                .orElseThrow();
        assertThat(precomputed).containsExactlyInAnyOrder("PAY", "SHIP", "CANCEL");

        List<String> resolved = constantExtractor.resolveMethodReturnConstant(
                "com.example.EventSource", "getEvent", 0, new HashSet<>(), null);
        assertThat(resolved).containsExactlyInAnyOrder("PAY", "SHIP", "CANCEL");
    }

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

        List<String> precomputed = context.getPolymorphicAccessorIndex()
                .widenedReturnConstants("com.example.EventSource", "getEvent")
                .orElseThrow();
        assertThat(precomputed).containsExactlyInAnyOrder("DEFAULT", "PAY", "SHIP", "CANCEL");
    }

    @Nested
    class AbstractStateMachineInheritance {

        @Test
        void shouldResolveEventsFromThreeConcreteConfigsExtendingAbstractBase() throws IOException {
            writeJava("com/example/OrderState.java", """
                    package com.example;
                    public enum OrderState { CREATED, PAID, SHIPPED, CANCELLED }
                    """);
            writeJava("com/example/OrderEvent.java", """
                    package com.example;
                    public enum OrderEvent { PAY, SHIP, CANCEL }
                    """);
            writeJava("com/example/EventSource.java", """
                    package com.example;
                    public interface EventSource {
                        OrderEvent getEvent();
                    }
                    """);
            writeJava("com/example/PayEvent.java", """
                    package com.example;
                    public class PayEvent implements EventSource {
                        public OrderEvent getEvent() { return OrderEvent.PAY; }
                    }
                    """);
            writeJava("com/example/ShipEvent.java", """
                    package com.example;
                    public class ShipEvent implements EventSource {
                        public OrderEvent getEvent() { return OrderEvent.SHIP; }
                    }
                    """);
            writeJava("com/example/CancelEvent.java", """
                    package com.example;
                    public class CancelEvent implements EventSource {
                        public OrderEvent getEvent() { return OrderEvent.CANCEL; }
                    }
                    """);
            writeJava("com/example/BaseOrderStateMachineConfig.java", """
                    package com.example;
                    import org.springframework.statemachine.config.StateMachineConfigurerAdapter;
                    import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer;
                    public abstract class BaseOrderStateMachineConfig
                            extends StateMachineConfigurerAdapter<OrderState, OrderEvent> {
                        protected void registerTransition(
                                StateMachineTransitionConfigurer<OrderState, OrderEvent> transitions,
                                EventSource source) {
                            transitions.withExternal()
                                    .source(OrderState.CREATED)
                                    .target(OrderState.PAID)
                                    .event(source.getEvent());
                        }
                    }
                    """);
            writeJava("com/example/PayOrderStateMachineConfig.java", """
                    package com.example;
                    import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer;
                    public class PayOrderStateMachineConfig extends BaseOrderStateMachineConfig {
                        @Override
                        public void configure(StateMachineTransitionConfigurer<OrderState, OrderEvent> transitions)
                                throws Exception {
                            registerTransition(transitions, new PayEvent());
                        }
                    }
                    """);
            writeJava("com/example/ShipOrderStateMachineConfig.java", """
                    package com.example;
                    import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer;
                    public class ShipOrderStateMachineConfig extends BaseOrderStateMachineConfig {
                        @Override
                        public void configure(StateMachineTransitionConfigurer<OrderState, OrderEvent> transitions)
                                throws Exception {
                            registerTransition(transitions, new ShipEvent());
                        }
                    }
                    """);
            writeJava("com/example/CancelOrderStateMachineConfig.java", """
                    package com.example;
                    import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer;
                    public class CancelOrderStateMachineConfig extends BaseOrderStateMachineConfig {
                        @Override
                        public void configure(StateMachineTransitionConfigurer<OrderState, OrderEvent> transitions)
                                throws Exception {
                            registerTransition(transitions, new CancelEvent());
                        }
                    }
                    """);
            scan();

            List<String> widened = context.getPolymorphicAccessorIndex()
                    .widenedReturnConstants("com.example.EventSource", "getEvent")
                    .orElseThrow();
            assertThat(widened).containsExactlyInAnyOrder("OrderEvent.PAY", "OrderEvent.SHIP", "OrderEvent.CANCEL");

            List<String> resolved = constantExtractor.resolveMethodReturnConstant(
                    "com.example.EventSource", "getEvent", 0, new HashSet<>(), null);
            assertThat(resolved).containsExactlyInAnyOrder("OrderEvent.PAY", "OrderEvent.SHIP", "OrderEvent.CANCEL");
        }
    }

    @Test
    void getImplementationsShouldBeMemoized() throws IOException {
        writeJava("com/example/EventSource.java", """
                package com.example;
                public interface EventSource {}
                """);
        writeJava("com/example/PayEvent.java", """
                package com.example;
                public class PayEvent implements EventSource {}
                """);
        scan();

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

        assertThat(second).isSameAs(first);
    }

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

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