package click.kamil.springstatemachineexporter.analysis.resolver;

import click.kamil.springstatemachineexporter.analysis.service.ConstantExtractor;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import org.eclipse.jdt.core.dom.*;
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 org.assertj.core.api.Assertions.assertThat;

public class ConstantResolverTest {

    @Test
    void testConcatenationSameClass(@TempDir Path tempDir) throws IOException {
        Path dir = tempDir.resolve("com/example");
        Files.createDirectories(dir);
        Files.writeString(dir.resolve("Constants.java"),
            "package com.example;\n" +
            "public class Constants {\n" +
            "    public static final String A = \"/a\";\n" +
            "    public static final String B = \"/b\";\n" +
            "    public static final String AB = A + B;\n" +
            "    public String usage = AB;\n" +
            "}");

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

        TypeDeclaration td = context.getTypeDeclaration("com.example.Constants");
        FieldDeclaration usageField = td.getFields()[3]; // usage
        VariableDeclarationFragment fragment = (VariableDeclarationFragment) usageField.fragments().get(0);
        
        ConstantResolver resolver = new ConstantResolver();
        String result = resolver.resolve(fragment.getInitializer(), context);
        
        assertThat(result).isEqualTo("/a/b");
    }

    @Test
    void testCrossClassConcatenation(@TempDir Path tempDir) throws IOException {
        Path dir = tempDir.resolve("com/example");
        Files.createDirectories(dir);
        Files.writeString(dir.resolve("Base.java"),
            "package com.example;\n" +
            "public class Base {\n" +
            "    public static final String PATH = \"/api\";\n" +
            "}");
        Files.writeString(dir.resolve("Endpoint.java"),
            "package com.example;\n" +
            "public class Endpoint {\n" +
            "    public static final String FULL = Base.PATH + \"/v1\";\n" +
            "}");

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

        TypeDeclaration td = context.getTypeDeclaration("com.example.Endpoint");
        FieldDeclaration field = td.getFields()[0];
        VariableDeclarationFragment fragment = (VariableDeclarationFragment) field.fragments().get(0);

        ConstantResolver resolver = new ConstantResolver();
        String result = resolver.resolve(fragment.getInitializer(), context);

        assertThat(result).isEqualTo("/api/v1");
    }

    @Test
    void testRecursiveResolution(@TempDir Path tempDir) throws IOException {
        Path dir = tempDir.resolve("com/example");
        Files.createDirectories(dir);
        Files.writeString(dir.resolve("Deep.java"),
            "package com.example;\n" +
            "public class Deep {\n" +
            "    public static final String V1 = \"/v1\";\n" +
            "    public static final String API = \"/api\" + V1;\n" +
            "    public static final String USERS = API + \"/users\";\n" +
            "}");

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

        TypeDeclaration td = context.getTypeDeclaration("com.example.Deep");
        FieldDeclaration field = td.getFields()[2]; // USERS
        VariableDeclarationFragment fragment = (VariableDeclarationFragment) field.fragments().get(0);

        ConstantResolver resolver = new ConstantResolver();
        String result = resolver.resolve(fragment.getInitializer(), context);

        assertThat(result).isEqualTo("/api/v1/users");
    }

    @Test
    void testCycleDetection(@TempDir Path tempDir) throws IOException {
        Path dir = tempDir.resolve("com/example");
        Files.createDirectories(dir);
        Files.writeString(dir.resolve("Cycle.java"),
            "package com.example;\n" +
            "public class Cycle {\n" +
            "    public static final String A = B;\n" +
            "    public static final String B = A;\n" +
            "}");

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

        TypeDeclaration td = context.getTypeDeclaration("com.example.Cycle");
        FieldDeclaration field = td.getFields()[0];
        VariableDeclarationFragment fragment = (VariableDeclarationFragment) field.fragments().get(0);

        ConstantResolver resolver = new ConstantResolver();
        // Should not crash
        String result = resolver.resolve(fragment.getInitializer(), context);
        assertThat(result).isNull();
    }

    @Test
    void testValueAnnotationSupport(@TempDir Path tempDir) throws IOException {
        Path dir = tempDir.resolve("com/example");
        Files.createDirectories(dir);
        Files.writeString(dir.resolve("Controller.java"),
            "package com.example;\n" +
            "import org.springframework.beans.factory.annotation.Value;\n" +
            "public class Controller {\n" +
            "    @Value(\"${app.path}\")\n" +
            "    private String path;\n" +
            "    \n" +
            "    public String getPath() { return path; }\n" +
            "}");

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

        TypeDeclaration td = context.getTypeDeclaration("com.example.Controller");
        FieldDeclaration field = td.getFields()[0];
        VariableDeclarationFragment fragment = (VariableDeclarationFragment) field.fragments().get(0);

        ConstantResolver resolver = new ConstantResolver();
        // Resolve a SimpleName referring to an @Value field
        SimpleName sn = td.getAST().newSimpleName("path");
        // We need to attach the SimpleName to the AST to find its enclosing type
        // Actually, resolveManual(SimpleName) uses findEnclosingType. 
        // In the test, we can manually trigger it or mock the node hierarchy.
        
        // Let's use a real usage in a method
        MethodDeclaration method = td.getMethods()[0];
        ReturnStatement rs = (ReturnStatement) method.getBody().statements().get(0);
        Expression returnExpr = rs.getExpression();

        String result = resolver.resolve(returnExpr, context);
        assertThat(result).isEqualTo("${app.path}");
    }

    @Test
    void testConstructorAssignmentAndFieldAccess(@TempDir Path tempDir) throws IOException {
        Path dir = tempDir.resolve("com/example");
        Files.createDirectories(dir);
        Files.writeString(dir.resolve("EventDto.java"),
            "package com.example;\n" +
            "public class EventDto {\n" +
            "    private String eventType;\n" +
            "    public EventDto() {\n" +
            "        this.eventType = \"PAYMENT_SUCCESS\";\n" +
            "    }\n" +
            "    public String getEventType() {\n" +
            "        return this.eventType;\n" +
            "    }\n" +
            "}");

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

        TypeDeclaration td = context.getTypeDeclaration("com.example.EventDto");
        MethodDeclaration method = td.getMethods()[1]; // getEventType
        ReturnStatement rs = (ReturnStatement) method.getBody().statements().get(0);
        Expression returnExpr = rs.getExpression(); // this.eventType

        ConstantResolver resolver = new ConstantResolver();
        String result = resolver.resolve(returnExpr, context);

        assertThat(result).isEqualTo("PAYMENT_SUCCESS");
    }

    @Test
    void testMethodEvaluationWithNestedBlocksAndVars(@TempDir Path tempDir) throws IOException {
        Path dir = tempDir.resolve("com/example");
        Files.createDirectories(dir);
        Files.writeString(dir.resolve("Logic.java"),
            "package com.example;\n" +
            "public class Logic {\n" +
            "    public String process(String event) {\n" +
            "        String myVar = event;\n" +
            "        if (myVar != null) {\n" +
            "            return myVar;\n" +
            "        }\n" +
            "        return \"DEFAULT\";\n" +
            "    }\n" +
            "}");

        Files.writeString(dir.resolve("Caller.java"),
            "package com.example;\n" +
            "public class Caller {\n" +
            "    public void call() {\n" +
            "        Logic logic = new Logic();\n" +
            "        String result = logic.process(\"MY_EVENT\");\n" +
            "    }\n" +
            "}");

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

        TypeDeclaration callerTd = context.getTypeDeclaration("com.example.Caller");
        MethodDeclaration callMethod = callerTd.getMethods()[0]; // call()
        
        VariableDeclarationStatement vds = (VariableDeclarationStatement) callMethod.getBody().statements().get(1);
        VariableDeclarationFragment fragment = (VariableDeclarationFragment) vds.fragments().get(0);
        MethodInvocation mi = (MethodInvocation) fragment.getInitializer(); // logic.process("MY_EVENT")

        ConstantResolver resolver = new ConstantResolver();
        String result = resolver.resolve(mi, context);

        assertThat(result).isEqualTo("MY_EVENT");
    }

    @Test
    void testMethodShadowingReturnsParameter(@TempDir Path tempDir) throws IOException {
        Path dir = tempDir.resolve("com/example");
        Files.createDirectories(dir);
        Files.writeString(dir.resolve("Logic.java"),
            "package com.example;\n" +
            "public class Logic {\n" +
            "    public String a = \"OLD_FIELD\";\n" +
            "    public String process(String a) {\n" + // shadowing
            "        this.a = \"NEW_FIELD\";\n" +
            "        return a;\n" + // should return the parameter
            "    }\n" +
            "}");

        Files.writeString(dir.resolve("Caller.java"),
            "package com.example;\n" +
            "public class Caller {\n" +
            "    public void call() {\n" +
            "        Logic logic = new Logic();\n" +
            "        String result = logic.process(\"PARAM_VALUE\");\n" +
            "    }\n" +
            "}");

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

        TypeDeclaration callerTd = context.getTypeDeclaration("com.example.Caller");
        MethodDeclaration callMethod = callerTd.getMethods()[0]; // call()
        
        VariableDeclarationStatement vds = (VariableDeclarationStatement) callMethod.getBody().statements().get(1);
        VariableDeclarationFragment fragment = (VariableDeclarationFragment) vds.fragments().get(0);
        MethodInvocation mi = (MethodInvocation) fragment.getInitializer(); // logic.process(...)

        ConstantResolver resolver = new ConstantResolver();
        String result = resolver.resolve(mi, context);

        assertThat(result).isEqualTo("PARAM_VALUE");
    }

    @Test
    void testMethodShadowingReturnsField(@TempDir Path tempDir) throws IOException {
        Path dir = tempDir.resolve("com/example");
        Files.createDirectories(dir);
        Files.writeString(dir.resolve("Logic.java"),
            "package com.example;\n" +
            "public class Logic {\n" +
            "    public String a = \"OLD_FIELD\";\n" +
            "    public String process(String a) {\n" + // shadowing
            "        this.a = \"NEW_FIELD_VALUE\";\n" +
            "        return this.a;\n" + // should return the field
            "    }\n" +
            "}");

        Files.writeString(dir.resolve("Caller.java"),
            "package com.example;\n" +
            "public class Caller {\n" +
            "    public void call() {\n" +
            "        Logic logic = new Logic();\n" +
            "        String result = logic.process(\"PARAM_VALUE\");\n" +
            "    }\n" +
            "}");

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

        TypeDeclaration callerTd = context.getTypeDeclaration("com.example.Caller");
        MethodDeclaration callMethod = callerTd.getMethods()[0]; // call()
        
        VariableDeclarationStatement vds = (VariableDeclarationStatement) callMethod.getBody().statements().get(1);
        VariableDeclarationFragment fragment = (VariableDeclarationFragment) vds.fragments().get(0);
        MethodInvocation mi = (MethodInvocation) fragment.getInitializer(); // logic.process(...)

        ConstantResolver resolver = new ConstantResolver();
        String result = resolver.resolve(mi, context);

        assertThat(result).isEqualTo("NEW_FIELD_VALUE");
    }

    @Test
    void testMethodImplicitFieldAssignment(@TempDir Path tempDir) throws IOException {
        Path dir = tempDir.resolve("com/example");
        Files.createDirectories(dir);
        Files.writeString(dir.resolve("Logic.java"),
            "package com.example;\n" +
            "public class Logic {\n" +
            "    public String a = \"OLD_FIELD\";\n" +
            "    public String process(String p) {\n" +
            "        a = p;\n" + // implicit field assignment
            "        return this.a;\n" +
            "    }\n" +
            "}");

        Files.writeString(dir.resolve("Caller.java"),
            "package com.example;\n" +
            "public class Caller {\n" +
            "    public void call() {\n" +
            "        Logic logic = new Logic();\n" +
            "        String result = logic.process(\"MY_EVENT\");\n" +
            "    }\n" +
            "}");

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

        TypeDeclaration callerTd = context.getTypeDeclaration("com.example.Caller");
        MethodDeclaration callMethod = callerTd.getMethods()[0]; // call()
        
        VariableDeclarationStatement vds = (VariableDeclarationStatement) callMethod.getBody().statements().get(1);
        VariableDeclarationFragment fragment = (VariableDeclarationFragment) vds.fragments().get(0);
        MethodInvocation mi = (MethodInvocation) fragment.getInitializer(); // logic.process(...)

        ConstantResolver resolver = new ConstantResolver();
        String result = resolver.resolve(mi, context);

        assertThat(result).isEqualTo("MY_EVENT");
    }

    @Test
    void testZeroArgMethodEvaluation(@TempDir Path tempDir) throws IOException {
        Path dir = tempDir.resolve("com/example");
        Files.createDirectories(dir);
        Files.writeString(dir.resolve("Logic.java"),
            "package com.example;\n" +
            "public class Logic {\n" +
            "    public String a = \"MY_CONSTANT\";\n" +
            "    public String getEvent() {\n" +
            "        return this.a;\n" +
            "    }\n" +
            "}");

        Files.writeString(dir.resolve("Caller.java"),
            "package com.example;\n" +
            "public class Caller {\n" +
            "    public void call() {\n" +
            "        Logic logic = new Logic();\n" +
            "        String result = logic.getEvent();\n" +
            "    }\n" +
            "}");

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

        TypeDeclaration callerTd = context.getTypeDeclaration("com.example.Caller");
        MethodDeclaration callMethod = callerTd.getMethods()[0]; // call()
        
        VariableDeclarationStatement vds = (VariableDeclarationStatement) callMethod.getBody().statements().get(1);
        VariableDeclarationFragment fragment = (VariableDeclarationFragment) vds.fragments().get(0);
        MethodInvocation mi = (MethodInvocation) fragment.getInitializer(); // logic.getEvent()

        ConstantResolver resolver = new ConstantResolver();
        String result = resolver.resolve(mi, context);

        assertThat(result).isEqualTo("MY_CONSTANT");
    }

    /**
     * When a switch method is called with a <em>known</em> constant argument, {@code evaluateMethodOutput}
     * should evaluate the switch and return only the matching branch.
     *
     * <p>Regression guard: before the {@code SwitchStatement.visit} fix, the ASTVisitor would also descend
     * into child {@code ReturnStatement} nodes after the switch was already handled, potentially overwriting
     * a correct result with a wrong one.
     */
    @Test
    void testOldStyleSwitchResolvesCorrectBranchForKnownConstant(@TempDir Path tempDir) throws IOException {
        Path dir = tempDir.resolve("com/example");
        Files.createDirectories(dir);
        Files.writeString(dir.resolve("Classifier.java"),
            "package com.example;\n" +
            "public class Classifier {\n" +
            "    public OrderEvents classify(String q) {\n" +
            "        switch (q) {\n" +
            "            case \"a\": return OrderEvents.A8;\n" +
            "            case \"b\": return OrderEvents.B2;\n" +
            "            default:  return OrderEvents.DEF;\n" +
            "        }\n" +
            "    }\n" +
            "}\n" +
            "enum OrderEvents { A8, B2, DEF }");
        Files.writeString(dir.resolve("Caller.java"),
            "package com.example;\n" +
            "public class Caller {\n" +
            "    public void call() {\n" +
            "        Classifier c = new Classifier();\n" +
            "        OrderEvents r = c.classify(\"a\");\n" + // known constant → should match case "a"
            "    }\n" +
            "}");

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

        TypeDeclaration callerTd = context.getTypeDeclaration("com.example.Caller");
        MethodDeclaration callMethod = callerTd.getMethods()[0];
        VariableDeclarationStatement vds = (VariableDeclarationStatement) callMethod.getBody().statements().get(1);
        VariableDeclarationFragment fragment = (VariableDeclarationFragment) vds.fragments().get(0);
        MethodInvocation mi = (MethodInvocation) fragment.getInitializer(); // c.classify("a")

        ConstantResolver resolver = new ConstantResolver();
        String result = resolver.resolve(mi, context);

        // Only the matching branch should be returned, not all branches
        assertThat(result).isEqualTo("OrderEvents.A8");
    }

    /**
     * When a switch method is called with a <em>runtime</em> variable that cannot be statically resolved,
     * {@code evaluateMethodOutput} must return {@code null} so the resolver falls back to the return-type
     * enum set (covering all branches).
     *
     * <p>Regression: before the fix, {@code SwitchStatement.visit} returned {@code super.visit(ss)} (true),
     * causing child {@code ReturnStatement} nodes inside the switch to fire. The first return — e.g.
     * {@code return OrderEvents.A8} — was captured as if it were the only possible result, silently
     * dropping the other branches. After the fix the visitor returns {@code false}, preventing child
     * traversal, so the resolver correctly falls back to the full ENUM_SET.
     */
    @Test
    void testOldStyleSwitchWithUnknownVariableReturnsFallbackEnumSet(@TempDir Path tempDir) throws IOException {
        Path dir = tempDir.resolve("com/example");
        Files.createDirectories(dir);
        Files.writeString(dir.resolve("Classifier.java"),
            "package com.example;\n" +
            "public class Classifier {\n" +
            "    public OrderEvents classify(String q) {\n" +
            "        switch (q) {\n" +
            "            case \"a\": return OrderEvents.A8;\n" +
            "            case \"b\": return OrderEvents.B2;\n" +
            "            default:  return OrderEvents.DEF;\n" +
            "        }\n" +
            "    }\n" +
            "}\n" +
            "enum OrderEvents { A8, B2, DEF }");
        Files.writeString(dir.resolve("Caller.java"),
            "package com.example;\n" +
            "public class Caller {\n" +
            "    public void call(String x) {\n" + // x is a runtime parameter — not a constant
            "        Classifier c = new Classifier();\n" +
            "        OrderEvents r = c.classify(x);\n" +
            "    }\n" +
            "}");

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

        TypeDeclaration callerTd = context.getTypeDeclaration("com.example.Caller");
        MethodDeclaration callMethod = callerTd.getMethods()[0];
        VariableDeclarationStatement vds = (VariableDeclarationStatement) callMethod.getBody().statements().get(1);
        VariableDeclarationFragment fragment = (VariableDeclarationFragment) vds.fragments().get(0);
        MethodInvocation mi = (MethodInvocation) fragment.getInitializer(); // c.classify(x)

        ConstantResolver resolver = new ConstantResolver();
        String result = resolver.resolve(mi, context);

        // Must cover ALL branches via ENUM_SET — not just the first one ("A8")
        assertThat(result).startsWith("ENUM_SET:");
        assertThat(result).contains("OrderEvents.A8");
        assertThat(result).contains("OrderEvents.B2");
        assertThat(result).contains("OrderEvents.DEF");
    }

    @Test
    void shouldResolveEnumValueOfWithConstantString(@TempDir Path tempDir) throws IOException {
        Path dir = tempDir.resolve("com/example");
        Files.createDirectories(dir);
        Files.writeString(dir.resolve("OrderEvent.java"),
            "package com.example;\n" +
            "public enum OrderEvent { RECEIVED, SHIPPED }\n");
        Files.writeString(dir.resolve("Caller.java"),
            "package com.example;\n" +
            "public class Caller {\n" +
            "    public void call() {\n" +
            "        OrderEvent e1 = OrderEvent.valueOf(\"RECEIVED\");\n" +
            "        OrderEvent e2 = OrderEvent.valueOf(getEventString());\n" +
            "    }\n" +
            "    private String getEventString() {\n" +
            "        return \"SHIPPED\";\n" +
            "    }\n" +
            "}");

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

        TypeDeclaration callerTd = context.getTypeDeclaration("com.example.Caller");
        MethodDeclaration callMethod = callerTd.getMethods()[0];
        
        VariableDeclarationStatement vds1 = (VariableDeclarationStatement) callMethod.getBody().statements().get(0);
        VariableDeclarationFragment fragment1 = (VariableDeclarationFragment) vds1.fragments().get(0);
        MethodInvocation mi1 = (MethodInvocation) fragment1.getInitializer();

        VariableDeclarationStatement vds2 = (VariableDeclarationStatement) callMethod.getBody().statements().get(1);
        VariableDeclarationFragment fragment2 = (VariableDeclarationFragment) vds2.fragments().get(0);
        MethodInvocation mi2 = (MethodInvocation) fragment2.getInitializer();

        ConstantResolver resolver = new ConstantResolver();
        
        String result1 = resolver.resolve(mi1, context);
        assertThat(result1).isEqualTo("com.example.OrderEvent.RECEIVED");

        String result2 = resolver.resolve(mi2, context);
        assertThat(result2).isEqualTo("com.example.OrderEvent.SHIPPED");
    }

    @Test
    void shouldResolveFullyQualifiedEnumValueOfWithoutTruncatingPackage(@TempDir Path tempDir) throws IOException {
        Path dir = tempDir.resolve("com/example");
        Files.createDirectories(dir);
        Files.writeString(dir.resolve("OrderEvent.java"),
            "package com.example;\n" +
            "public enum OrderEvent { PAY, SHIP }\n");
        Files.writeString(dir.resolve("Caller.java"),
            "package com.example;\n" +
            "public class Caller {\n" +
            "    public void call() {\n" +
            "        OrderEvent e = com.example.OrderEvent.valueOf(\"PAY\");\n" +
            "    }\n" +
            "}");

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

        TypeDeclaration callerTd = context.getTypeDeclaration("com.example.Caller");
        MethodDeclaration callMethod = callerTd.getMethods()[0];
        VariableDeclarationStatement vds = (VariableDeclarationStatement) callMethod.getBody().statements().get(0);
        VariableDeclarationFragment fragment = (VariableDeclarationFragment) vds.fragments().get(0);
        MethodInvocation mi = (MethodInvocation) fragment.getInitializer();

        String result = new ConstantResolver().resolve(mi, context);
        assertThat(result)
                .as("must not truncate com.example.OrderEvent to package prefix com.example")
                .isEqualTo("com.example.OrderEvent.PAY")
                .doesNotContain("com.example.PAY");
    }

    @Test
    void shouldResolveMapOfGetWithLiteralKey(@TempDir Path tempDir) throws IOException {
        Path dir = tempDir.resolve("com/example");
        Files.createDirectories(dir);
        Files.writeString(dir.resolve("Routes.java"),
            "package com.example;\n" +
            "public class Routes {\n" +
            "    public static final String PAY = \"OrderEvents.PAY\";\n" +
            "    public static final String SHIP = \"OrderEvents.SHIP\";\n" +
            "    public String lookup() {\n" +
            "        return java.util.Map.of(\"order.pay\", PAY, \"order.ship\", SHIP).get(\"order.pay\");\n" +
            "    }\n" +
            "}");

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

        TypeDeclaration td = context.getTypeDeclaration("com.example.Routes");
        MethodDeclaration lookup = td.getMethods()[0];
        ReturnStatement rs = (ReturnStatement) lookup.getBody().statements().get(0);

        ConstantResolver resolver = new ConstantResolver();
        String result = resolver.resolve(rs.getExpression(), context);

        assertThat(result).isEqualTo("OrderEvents.PAY");
    }

    @Test
    void shouldResolveStaticMapFieldGetWithLiteralKey(@TempDir Path tempDir) throws IOException {
        Path dir = tempDir.resolve("com/example");
        Files.createDirectories(dir);
        Files.writeString(dir.resolve("Dispatcher.java"),
            "package com.example;\n" +
            "import java.util.Map;\n" +
            "public class Dispatcher {\n" +
            "    private static final Map<String, String> ROUTES = Map.of(\n" +
            "        \"order.pay\", \"OrderEvents.PAY\",\n" +
            "        \"order.ship\", \"OrderEvents.SHIP\"\n" +
            "    );\n" +
            "    public String route() {\n" +
            "        return ROUTES.get(\"order.ship\");\n" +
            "    }\n" +
            "}");

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

        TypeDeclaration td = context.getTypeDeclaration("com.example.Dispatcher");
        MethodDeclaration route = td.getMethods()[0];
        ReturnStatement rs = (ReturnStatement) route.getBody().statements().get(0);

        ConstantResolver resolver = new ConstantResolver();
        String result = resolver.resolve(rs.getExpression(), context);

        assertThat(result).isEqualTo("OrderEvents.SHIP");
    }

    @Test
    void shouldResolveLiteralStringTransforms(@TempDir Path tempDir) throws IOException {
        Path dir = tempDir.resolve("com/example");
        Files.createDirectories(dir);
        Files.writeString(dir.resolve("OrderEvent.java"),
            "package com.example;\n" +
            "public enum OrderEvent { PAY, SHIP }\n");
        Files.writeString(dir.resolve("Caller.java"),
            "package com.example;\n" +
            "public class Caller {\n" +
            "    public OrderEvent fromLiteral() {\n" +
            "        return OrderEvent.valueOf(\"pay\".toUpperCase());\n" +
            "    }\n" +
            "    public String trimmed() {\n" +
            "        return \"  pay  \".trim().toUpperCase();\n" +
            "    }\n" +
            "}");

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

        TypeDeclaration td = context.getTypeDeclaration("com.example.Caller");
        MethodDeclaration fromLiteral = td.getMethods()[0];
        MethodDeclaration trimmed = td.getMethods()[1];
        ReturnStatement rs1 = (ReturnStatement) fromLiteral.getBody().statements().get(0);
        ReturnStatement rs2 = (ReturnStatement) trimmed.getBody().statements().get(0);

        ConstantResolver resolver = new ConstantResolver();

        assertThat(resolver.resolve(rs1.getExpression(), context)).isEqualTo("com.example.OrderEvent.PAY");
        assertThat(resolver.resolve(rs2.getExpression(), context)).isEqualTo("PAY");
    }

    @Test
    void shouldResolveStaticFieldUsingCallPathScope(@TempDir Path tempDir) throws IOException {
        Path dir = tempDir.resolve("com/example");
        Files.createDirectories(dir);
        Files.writeString(dir.resolve("ApiController.java"),
            "package com.example;\n" +
            "import java.util.Map;\n" +
            "public class ApiController {\n" +
            "    private static final Map<String, OrderEvent> ROUTES = Map.of(\n" +
            "        \"order.pay\", OrderEvent.PAY,\n" +
            "        \"order.ship\", OrderEvent.SHIP\n" +
            "    );\n" +
            "    public void dispatch(String commandKey) {\n" +
            "        OrderEvent event = ROUTES.get(commandKey);\n" +
            "    }\n" +
            "}\n" +
            "enum OrderEvent { PAY, SHIP }\n");

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

        ASTParser parser = ASTParser.newParser(AST.JLS17);
        parser.setKind(ASTParser.K_COMPILATION_UNIT);
        parser.setSource(("class A { Object o = ROUTES.get(commandKey); }").toCharArray());
        CompilationUnit cu = (CompilationUnit) parser.createAST(null);
        FieldDeclaration fd = (FieldDeclaration) ((TypeDeclaration) cu.types().get(0)).bodyDeclarations().get(0);
        MethodInvocation getCall = (MethodInvocation) ((VariableDeclarationFragment) fd.fragments().get(0)).getInitializer();

        ConstantExtractor extractor = new ConstantExtractor(context, new ConstantResolver(), mi -> null);
        context.setAnalysisCallPath(List.of("com.example.ApiController.dispatch"));
        try {
            ConstantResolver resolver = new ConstantResolver();
            SimpleName routesName = (SimpleName) getCall.getExpression();
            assertThat(resolver.resolve(routesName, context))
                    .as("static ROUTES field should resolve to encoded map")
                    .startsWith("MAP:");

            List<String> constants = new ArrayList<>();
            extractor.extractConstantsFromExpression(getCall, constants);
            assertThat(constants).contains("OrderEvent.PAY", "OrderEvent.SHIP");
        } finally {
            context.clearAnalysisCallPath();
        }
    }

    @Test
    void shouldResolveArrayIndexWithLiteralMath(@TempDir Path tempDir) throws IOException {
        Path dir = tempDir.resolve("com/example");
        Files.createDirectories(dir);
        Files.writeString(dir.resolve("Table.java"),
            "package com.example;\n" +
            "public class Table {\n" +
            "    private static final String[] EVENTS = {\"OrderEvents.PAY\", \"OrderEvents.SHIP\", \"OrderEvents.CANCEL\"};\n" +
            "    public String pick() {\n" +
            "        return EVENTS[1 + 1];\n" +
            "    }\n" +
            "}");

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

        TypeDeclaration td = context.getTypeDeclaration("com.example.Table");
        MethodDeclaration pick = td.getMethods()[0];
        ReturnStatement rs = (ReturnStatement) pick.getBody().statements().get(0);

        ConstantResolver resolver = new ConstantResolver();
        String result = resolver.resolve(rs.getExpression(), context);

        assertThat(result).isEqualTo("OrderEvents.CANCEL");
    }
}
