package click.kamil.springstatemachineexporter.analysis.service;

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.ast.common.CodebaseContext;
import org.eclipse.jdt.core.dom.Expression;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.ReturnStatement;
import org.eclipse.jdt.core.dom.TypeDeclaration;
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.ArrayList;
import java.util.List;

import static click.kamil.springstatemachineexporter.analysis.service.CentralDispatcherTestSupport.*;
import static org.assertj.core.api.Assertions.assertThat;

/**
 * Robust coverage for compile-time literal dataflow: map lookup, string transforms, array indexing,
 * extractor expansion, and call-graph end-to-end resolution.
 */
class LiteralDataFlowResolutionTest {

    private static CodebaseContext scan(Path tempDir, String fileName, String source) throws IOException {
        Files.writeString(tempDir.resolve(fileName), source);
        CodebaseContext context = new CodebaseContext();
        context.scan(tempDir);
        return context;
    }

    private static Expression returnExpression(Path tempDir, String fileName, String source, String typeName, String methodName)
            throws IOException {
        CodebaseContext context = scan(tempDir, fileName, source);
        TypeDeclaration td = context.getTypeDeclaration(typeName);
        MethodDeclaration method = findMethod(td, methodName);
        ReturnStatement rs = (ReturnStatement) method.getBody().statements().get(0);
        return rs.getExpression();
    }

    private static MethodDeclaration findMethod(TypeDeclaration td, String methodName) {
        for (MethodDeclaration md : td.getMethods()) {
            if (methodName.equals(md.getName().getIdentifier())) {
                return md;
            }
        }
        throw new IllegalArgumentException("Method not found: " + methodName);
    }

    @Nested
    class ConstantResolverCases {

        @Test
        void shouldReturnNullForMapGetWithUnknownLiteralKey(@TempDir Path tempDir) throws IOException {
            Expression expr = returnExpression(tempDir, "Routes.java", """
                package com.example;
                import java.util.Map;
                public class Routes {
                    private static final Map<String, String> ROUTES = Map.of(
                        "order.pay", "OrderEvent.PAY",
                        "order.ship", "OrderEvent.SHIP"
                    );
                    public String miss() {
                        return ROUTES.get("order.cancel");
                    }
                }
                """, "com.example.Routes", "miss");

            CodebaseContext context = new CodebaseContext();
            context.scan(tempDir);

            assertThat(new ConstantResolver().resolve(expr, context)).isNull();
        }

        @Test
        void shouldReturnNullForMapGetWithDynamicKey(@TempDir Path tempDir) throws IOException {
            Expression expr = returnExpression(tempDir, "Routes.java", """
                package com.example;
                import java.util.Map;
                public class Routes {
                    private static final Map<String, String> ROUTES = Map.of(
                        "order.pay", "OrderEvent.PAY",
                        "order.ship", "OrderEvent.SHIP"
                    );
                    public String dynamic(String commandKey) {
                        return ROUTES.get(commandKey);
                    }
                }
                """, "com.example.Routes", "dynamic");

            CodebaseContext context = new CodebaseContext();
            context.scan(tempDir);

            assertThat(new ConstantResolver().resolve(expr, context)).isNull();
        }

        @Test
        void shouldResolveMapOfEntriesWithLiteralKey(@TempDir Path tempDir) throws IOException {
            Expression expr = returnExpression(tempDir, "Routes.java", """
                package com.example;
                import java.util.Map;
                public class Routes {
                    public String lookup() {
                        return Map.ofEntries(
                            Map.entry("order.pay", "OrderEvent.PAY"),
                            Map.entry("order.ship", "OrderEvent.SHIP")
                        ).get("order.pay");
                    }
                }
                """, "com.example.Routes", "lookup");

            CodebaseContext context = new CodebaseContext();
            context.scan(tempDir);

            assertThat(new ConstantResolver().resolve(expr, context)).isEqualTo("OrderEvent.PAY");
        }

        @Test
        void shouldResolveMapGetOrDefaultWithLiteralKey(@TempDir Path tempDir) throws IOException {
            Expression expr = returnExpression(tempDir, "Routes.java", """
                package com.example;
                import java.util.Map;
                public class Routes {
                    private static final Map<String, String> ROUTES = Map.of("order.pay", "OrderEvent.PAY");
                    public String lookup() {
                        return ROUTES.getOrDefault("order.pay", "OrderEvent.FALLBACK");
                    }
                }
                """, "com.example.Routes", "lookup");

            CodebaseContext context = new CodebaseContext();
            context.scan(tempDir);

            assertThat(new ConstantResolver().resolve(expr, context)).isEqualTo("OrderEvent.PAY");
        }

        @Test
        void shouldNotFoldToUpperCaseOnVariableReceiver(@TempDir Path tempDir) throws IOException {
            Expression expr = returnExpression(tempDir, "Caller.java", """
                package com.example;
                public class Caller {
                    public String dynamic(String action) {
                        return action.toUpperCase();
                    }
                }
                """, "com.example.Caller", "dynamic");

            CodebaseContext context = new CodebaseContext();
            context.scan(tempDir);

            assertThat(new ConstantResolver().resolve(expr, context)).isNull();
        }

        @Test
        void shouldFoldToLowerCaseOnLiteralReceiver(@TempDir Path tempDir) throws IOException {
            Expression expr = returnExpression(tempDir, "Caller.java", """
                package com.example;
                public class Caller {
                    public String lower() {
                        return "PAY".toLowerCase();
                    }
                }
                """, "com.example.Caller", "lower");

            CodebaseContext context = new CodebaseContext();
            context.scan(tempDir);

            assertThat(new ConstantResolver().resolve(expr, context)).isEqualTo("pay");
        }

        @Test
        void shouldResolveInlineArrayLiteralWithIndexMath(@TempDir Path tempDir) throws IOException {
            Expression expr = returnExpression(tempDir, "Table.java", """
                package com.example;
                public class Table {
                    public String pick() {
                        return new String[] {"OrderEvent.PAY", "OrderEvent.SHIP", "OrderEvent.CANCEL"}[1 + 1];
                    }
                }
                """, "com.example.Table", "pick");

            CodebaseContext context = new CodebaseContext();
            context.scan(tempDir);

            assertThat(new ConstantResolver().resolve(expr, context)).isEqualTo("OrderEvent.CANCEL");
        }

        @Test
        void shouldResolveArrayIndexWithSubtraction(@TempDir Path tempDir) throws IOException {
            Expression expr = returnExpression(tempDir, "Table.java", """
                package com.example;
                public class Table {
                    private static final String[] EVENTS = {"OrderEvent.PAY", "OrderEvent.SHIP", "OrderEvent.CANCEL"};
                    public String pick() {
                        return EVENTS[2 - 1];
                    }
                }
                """, "com.example.Table", "pick");

            CodebaseContext context = new CodebaseContext();
            context.scan(tempDir);

            assertThat(new ConstantResolver().resolve(expr, context)).isEqualTo("OrderEvent.SHIP");
        }

        @Test
        void shouldReturnNullForDynamicArrayIndex(@TempDir Path tempDir) throws IOException {
            Expression expr = returnExpression(tempDir, "Table.java", """
                package com.example;
                public class Table {
                    private static final String[] EVENTS = {"OrderEvent.PAY", "OrderEvent.SHIP"};
                    public String pick(int idx) {
                        return EVENTS[idx];
                    }
                }
                """, "com.example.Table", "pick");

            CodebaseContext context = new CodebaseContext();
            context.scan(tempDir);

            assertThat(new ConstantResolver().resolve(expr, context)).isNull();
        }
    }

    @Nested
    class ConstantExtractorCases {

        private List<String> extract(String source, String typeName, String methodName, Path tempDir) throws IOException {
            Expression expr = returnExpression(tempDir, "App.java", source, typeName, methodName);
            CodebaseContext context = new CodebaseContext();
            context.scan(tempDir);
            ConstantExtractor extractor = new ConstantExtractor(context, new ConstantResolver(), mi -> null);
            List<String> constants = new ArrayList<>();
            extractor.extractConstantsFromExpression(expr, constants);
            return constants;
        }

        @Test
        void bareMapOfShouldExpandAllValuesNotRawMapEncoding(@TempDir Path tempDir) throws IOException {
            List<String> constants = extract("""
                package com.example;
                import java.util.Map;
                public class Routes {
                    public Map<String, String> all() {
                        return Map.of("order.pay", "OrderEvent.PAY", "order.ship", "OrderEvent.SHIP");
                    }
                }
                """, "com.example.Routes", "all", tempDir);

            assertThat(constants)
                    .containsExactlyInAnyOrder("OrderEvent.PAY", "OrderEvent.SHIP")
                    .noneMatch(value -> value.startsWith("MAP:"));
        }

        @Test
        void mapGetWithLiteralKeyShouldExtractSingleValue(@TempDir Path tempDir) throws IOException {
            List<String> constants = extract("""
                package com.example;
                import java.util.Map;
                public class Routes {
                    public String one() {
                        return Map.of("order.pay", "OrderEvent.PAY", "order.ship", "OrderEvent.SHIP")
                                .get("order.pay");
                    }
                }
                """, "com.example.Routes", "one", tempDir);

            assertThat(constants).containsExactly("OrderEvent.PAY");
        }

        @Test
        void staticMapGetWithDynamicKeyShouldExpandAllRouteValues(@TempDir Path tempDir) throws IOException {
            List<String> constants = extract("""
                package com.example;
                import java.util.Map;
                public class Routes {
                    private static final Map<String, String> ROUTES = Map.of(
                        "order.pay", "OrderEvent.PAY",
                        "order.ship", "OrderEvent.SHIP"
                    );
                    public String dynamic(String commandKey) {
                        return ROUTES.get(commandKey);
                    }
                }
                """, "com.example.Routes", "dynamic", tempDir);

            assertThat(constants).contains("OrderEvent.PAY", "OrderEvent.SHIP");
        }

        @Test
        void bareArrayLiteralShouldExpandAllElements(@TempDir Path tempDir) throws IOException {
            List<String> constants = extract("""
                package com.example;
                public class Table {
                    public String[] all() {
                        return new String[] {"OrderEvent.PAY", "OrderEvent.SHIP", "OrderEvent.CANCEL"};
                    }
                }
                """, "com.example.Table", "all", tempDir);

            assertThat(constants)
                    .containsExactlyInAnyOrder("OrderEvent.PAY", "OrderEvent.SHIP", "OrderEvent.CANCEL")
                    .noneMatch(value -> value.startsWith("ARRAY:"));
        }
    }

    @Nested
    class CallGraphIntegrationCases {

        private static final String CONTROLLER = "com.example.ApiController";
        private static final String STATE_MACHINE = "com.example.StateMachine";

        @Test
        void staticRoutesLiteralKeyShouldResolveSinglePolymorphicEvent(@TempDir Path tempDir) throws Exception {
            String source = """
                package com.example;
                import java.util.Map;
                public class ApiController {
                    StateMachine sm;
                    public void payOrder() {
                        sm.sendEvent(Routes.ROUTES.get("order.pay"));
                    }
                }
                class Routes {
                    static final Map<String, OrderEvent> ROUTES = Map.of(
                        "order.pay", OrderEvent.PAY,
                        "order.ship", OrderEvent.SHIP,
                        "order.cancel", OrderEvent.CANCEL
                    );
                }
                enum OrderEvent { PAY, SHIP, CANCEL }
                class StateMachine { public void sendEvent(OrderEvent e) {} }
                """;
            CodebaseContext context = scanSource(source, tempDir);

            CallChain chain = resolveChain(context, CONTROLLER, "payOrder", STATE_MACHINE);
            assertPolyEvents(chain, "OrderEvent.PAY");
        }

        @Test
        void literalToUpperCaseBeforeValueOfShouldResolveSingleEvent(@TempDir Path tempDir) throws Exception {
            String source = """
                package com.example;
                public class ApiController {
                    StateMachine sm;
                    public void payOrder() {
                        sm.sendEvent(OrderEvent.valueOf("pay".toUpperCase()));
                    }
                }
                enum OrderEvent { PAY, SHIP, CANCEL }
                class StateMachine { public void sendEvent(OrderEvent e) {} }
                """;
            CodebaseContext context = scanSource(source, tempDir);

            CallChain chain = resolveChain(context, CONTROLLER, "payOrder", STATE_MACHINE);
            assertPolyEvents(chain, "OrderEvent.PAY");
        }

        @Test
        void variableToUpperCaseBeforeValueOfShouldNotNarrowToSingleEvent(@TempDir Path tempDir) throws Exception {
            String source = """
                package com.example;
                public class ApiController {
                    StateMachine sm;
                    public void processAction(String action) {
                        sm.sendEvent(OrderEvent.valueOf(action.toUpperCase()));
                    }
                }
                enum OrderEvent { PAY, SHIP, CANCEL }
                class StateMachine { public void sendEvent(OrderEvent e) {} }
                """;
            CodebaseContext context = scanSource(source, tempDir);

            HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context);
            EntryPoint entryPoint = EntryPoint.builder()
                    .className(CONTROLLER)
                    .methodName("processAction")
                    .build();
            TriggerPoint trigger = TriggerPoint.builder()
                    .className(STATE_MACHINE)
                    .methodName("sendEvent")
                    .event("e")
                    .build();

            CallChain chain = engine.findChains(List.of(entryPoint), List.of(trigger)).get(0);
            assertThat(chain.getTriggerPoint().getPolymorphicEvents())
                    .contains("OrderEvent.PAY", "OrderEvent.SHIP", "OrderEvent.CANCEL");
        }

        @Test
        void arrayIndexMathShouldResolveSinglePolymorphicEvent(@TempDir Path tempDir) throws Exception {
            String source = """
                package com.example;
                public class ApiController {
                    StateMachine sm;
                    public void cancelOrder() {
                        sm.sendEvent(RoutingTable.EVENTS[1 + 1]);
                    }
                }
                class RoutingTable {
                    static final OrderEvent[] EVENTS = {OrderEvent.PAY, OrderEvent.SHIP, OrderEvent.CANCEL};
                }
                enum OrderEvent { PAY, SHIP, CANCEL }
                class StateMachine { public void sendEvent(OrderEvent e) {} }
                """;
            CodebaseContext context = scanSource(source, tempDir);

            CallChain chain = resolveChain(context, CONTROLLER, "cancelOrder", STATE_MACHINE);
            assertPolyEvents(chain, "OrderEvent.CANCEL");
        }

        @Test
        void mapGetWithDynamicKeyShouldUnionAllRouteEvents(@TempDir Path tempDir) throws Exception {
            String source = """
                package com.example;
                import java.util.Map;
                public class ApiController {
                    StateMachine sm;
                    private static final Map<String, OrderEvent> ROUTES = Map.of(
                        "order.pay", OrderEvent.PAY,
                        "order.ship", OrderEvent.SHIP
                    );
                    public void dispatch(String commandKey) {
                        sm.sendEvent(ROUTES.get(commandKey));
                    }
                }
                enum OrderEvent { PAY, SHIP }
                class StateMachine { public void sendEvent(OrderEvent e) {} }
                """;
            CodebaseContext context = scanSource(source, tempDir);

            CallChain chain = resolveChain(context, CONTROLLER, "dispatch", STATE_MACHINE);
            assertThat(chain.getTriggerPoint().getPolymorphicEvents())
                    .contains("OrderEvent.PAY", "OrderEvent.SHIP");
        }

        @Test
        void crossClassStaticMapGetWithDynamicKeyShouldUnionAllRouteEvents(@TempDir Path tempDir) throws Exception {
            String source = """
                package com.example;
                import java.util.Map;
                public class ApiController {
                    StateMachine sm;
                    public void dispatch(String commandKey) {
                        sm.sendEvent(Routes.ROUTES.get(commandKey));
                    }
                }
                class Routes {
                    static final Map<String, OrderEvent> ROUTES = Map.of(
                        "order.pay", OrderEvent.PAY,
                        "order.ship", OrderEvent.SHIP
                    );
                }
                enum OrderEvent { PAY, SHIP }
                class StateMachine { public void sendEvent(OrderEvent e) {} }
                """;
            CodebaseContext context = scanSource(source, tempDir);

            CallChain chain = resolveChain(context, CONTROLLER, "dispatch", STATE_MACHINE);
            assertThat(chain.getTriggerPoint().getPolymorphicEvents())
                    .contains("OrderEvent.PAY", "OrderEvent.SHIP");
        }
    }
}
