package click.kamil.springstatemachineexporter.ast.app;

import click.kamil.springstatemachineexporter.model.Action;
import click.kamil.springstatemachineexporter.model.Guard;
import click.kamil.springstatemachineexporter.model.State;
import click.kamil.springstatemachineexporter.model.Transition;
import click.kamil.springstatemachineexporter.model.TransitionType;
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
import org.eclipse.jdt.core.dom.AST;
import org.eclipse.jdt.core.dom.ASTParser;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.TypeDeclaration;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.util.List;

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

class AstTransitionParserTest {

    private CodebaseContext context;

    @BeforeEach
    void setUp() {
        context = new CodebaseContext();
    }

    @Test
    void shouldParseSimpleWithExternalTransition() {
        String source = """
                public class TestClass {
                    public void configure(StateMachineTransitionConfigurer<String, String> transitions) throws Exception {
                        transitions.withExternal().source("CREATED").target("PAID").event("PAY");
                    }
                }
                """;
        MethodDeclaration method = createMethodDeclaration(source);

        List<Transition> transitions = AstTransitionParser.parseTransitions(method, context);

        assertThat(transitions).hasSize(1);
        Transition transition = transitions.getFirst();
        assertThat(transition)
                .extracting(Transition::getType)
                .isEqualTo(TransitionType.EXTERNAL);
        assertThat(transition.getSourceStates()).extracting(State::toString).containsExactly("CREATED");
        assertThat(transition.getTargetStates()).extracting(State::toString).containsExactly("PAID");
        assertThat(transition.getEvent().toString()).isEqualTo("PAY");
    }

    @Test
    void shouldResolveStaticImportsForStates() {
        String source = """
                import static com.example.OrderStates.CREATED;
                import static com.example.OrderStates.PAID;
                public class TestClass {
                    public void configure(StateMachineTransitionConfigurer transitions) throws Exception {
                        transitions.withExternal().source(CREATED).target(PAID).event("PAY");
                    }
                }
                """;
        MethodDeclaration method = createMethodDeclaration(source);

        List<Transition> transitions = AstTransitionParser.parseTransitions(method, context);

        assertThat(transitions).hasSize(1);
        System.out.println("Actual source state: " + transitions.getFirst().getSourceStates().get(0));
        assertThat(transitions.getFirst().getSourceStates()).extracting(State::toString).containsExactly("com.example.OrderStates.CREATED");
        assertThat(transitions.getFirst().getTargetStates()).extracting(State::toString).containsExactly("com.example.OrderStates.PAID");
    }

    @Test
    void shouldResolveQualifiedNamesForStates() {
        String source = """
                public class TestClass {
                    public void configure(StateMachineTransitionConfigurer transitions) throws Exception {
                        transitions.withExternal().source(OrderStates.CREATED).target(OrderStates.PAID);
                    }
                }
                """;
        MethodDeclaration method = createMethodDeclaration(source);

        List<Transition> transitions = AstTransitionParser.parseTransitions(method, context);

        assertThat(transitions).hasSize(1);
        // Without full scan, OrderStates is treated as a simple qualifier
        assertThat(transitions.getFirst().getSourceStates()).extracting(State::toString).containsExactly("OrderStates.CREATED");
    }

    @Test
    void shouldParseTransitionWithGuard() {
        String source = """
                public class TestClass {
                    public void configure(StateMachineTransitionConfigurer<String, String> transitions) throws Exception {
                        transitions.withExternal().source("CREATED").target("PAID").event("PAY").guard("amount > 0");
                    }
                }
                """;
        MethodDeclaration method = createMethodDeclaration(source);

        List<Transition> transitions = AstTransitionParser.parseTransitions(method, context);

        assertThat(transitions).hasSize(1);
        Transition transition = transitions.getFirst();
        assertThat(transition.getGuards().getFirst())
                .extracting(Guard::expression)
                .isEqualTo("amount > 0");
    }

    @Test
    void shouldParseTransitionWithLambdaGuardAndAction() {
        String source = """
                public class TestClass {
                    public void configure(StateMachineTransitionConfigurer<String, String> transitions) throws Exception {
                        transitions.withExternal()
                            .source("CREATED")
                            .target("PAID")
                            .event("PAY")
                            .guard(context -> true)
                            .action(context -> System.out.println("Action"));
                    }
                }
                """;
        MethodDeclaration method = createMethodDeclaration(source);

        List<Transition> transitions = AstTransitionParser.parseTransitions(method, context);

        assertThat(transitions).hasSize(1);
        Transition transition = transitions.getFirst();
        assertThat(transition.getGuards().getFirst().isLambda()).isTrue();
        assertThat(transition.getActions())
                .extracting(Action::isLambda)
                .containsExactly(true);
    }

    @Test
    void shouldParseMultipleTransitionsWithAnd() {
        String source = """
                public class TestClass {
                    public void configure(StateMachineTransitionConfigurer<String, String> transitions) throws Exception {
                        transitions.withExternal().source("CREATED").target("PAID").event("PAY").and().withExternal().source("PAID").target("SHIPPED").event("SHIP");
                    }
                }
                """;
        MethodDeclaration method = createMethodDeclaration(source);

        List<Transition> transitions = AstTransitionParser.parseTransitions(method, context);

        assertThat(transitions).hasSize(2);
    }

    @Test
    void shouldParseWithChoiceTransition() {
        String source = """
                public class TestClass {
                    public void configure(StateMachineTransitionConfigurer<String, String> transitions) throws Exception {
                        transitions.withChoice().source("PAID").first("SHIPPED", "isExpress()").then("CANCELLED", "isCancelled()").last("PENDING");
                    }
                }
                """;
        MethodDeclaration method = createMethodDeclaration(source);

        List<Transition> transitions = AstTransitionParser.parseTransitions(method, context);

        assertThat(transitions).hasSize(3);
        assertThat(transitions.getFirst().getSourceStates()).extracting(State::toString).containsExactly("PAID");
    }

    @Test
    void shouldDetectAnonymousClassAsLambda() {
        String source = """
                public class TestClass {
                    public void configure(StateMachineTransitionConfigurer<String, String> transitions) throws Exception {
                        transitions
                            .withExternal()
                            .source("S1")
                            .target("S2")
                            .action(new Action<String, String>() {
                                @Override
                                public void execute(StateContext<String, String> context) {}
                            });
                    }
                }
                """;
        MethodDeclaration method = createMethodDeclaration(source);

        List<Transition> transitions = AstTransitionParser.parseTransitions(method, context);

        assertThat(transitions).hasSize(1);
        assertThat(transitions.getFirst().getActions())
                .extracting(Action::isLambda)
                .containsExactly(true);
    }

    @Test
    void shouldParseTransitionsWithVariableDeclarations() {
        String source = """
                public class TestClass {
                    public void configure(StateMachineTransitionConfigurer<String, String> transitions) throws Exception {
                        ExternalTransitionConfigurer<String, String> t1 = transitions.withExternal().source("S1").target("S2").event("E1");
                        t1.and().withExternal().source("S2").target("S3").event("E2");
                    }
                }
                """;
        MethodDeclaration method = createMethodDeclaration(source);

        List<Transition> transitions = AstTransitionParser.parseTransitions(method, context);

        assertThat(transitions).hasSize(2);
        assertThat(transitions.get(0).getEvent().toString()).isEqualTo("E1");
        assertThat(transitions.get(1).getEvent().toString()).isEqualTo("E2");
    }

    @Test
    void shouldExtractInternalLogicForLambda() {
        String source = """
                public class TestClass {
                    public void configure(StateMachineTransitionConfigurer<String, String> transitions) throws Exception {
                        transitions.withExternal()
                            .source("S1")
                            .target("S2")
                            .guard(context -> context.getMessage() != null);
                    }
                }
                """;
        MethodDeclaration method = createMethodDeclaration(source);

        List<Transition> transitions = AstTransitionParser.parseTransitions(method, context);

        assertThat(transitions).hasSize(1);
        assertThat(transitions.getFirst().getGuards().getFirst().internalLogic())
                .contains("context.getMessage() != null");
    }

    @Test
    void shouldExtractInternalLogicForAnonymousClass() {
        String source = """
                public class TestClass {
                    public void configure(StateMachineTransitionConfigurer<String, String> transitions) throws Exception {
                        transitions
                            .withExternal()
                            .source("S1")
                            .target("S2")
                            .action(new Action<String, String>() {
                                @Override
                                public void execute(StateContext<String, String> context) {
                                    System.out.println("Hello");
                                }
                            });
                    }
                }
                """;
        MethodDeclaration method = createMethodDeclaration(source);

        List<Transition> transitions = AstTransitionParser.parseTransitions(method, context);

        assertThat(transitions).hasSize(1);
        assertThat(transitions.getFirst().getActions().get(0).internalLogic())
                .contains("System.out.println(\"Hello\")");
    }

    @Test
    void shouldExtractInternalLogicForSameFileMethodReturningLambda() {
        String source = """
                import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer;
                import org.springframework.statemachine.guard.Guard;
                public class TestClass {
                    public void configure(StateMachineTransitionConfigurer<String, String> transitions) throws Exception {
                        transitions
                            .withExternal()
                            .source("S1")
                            .target("S2")
                            .guard(guardVarEquals("test"));
                    }
                    
                    protected Guard<String, String> guardVarEquals(String expected) {
                        return context -> {
                            return expected.equals(context.getMessage());
                        };
                    }
                }
                """;
        MethodDeclaration method = createMethodDeclarationWithBindings(source);

        List<Transition> transitions = AstTransitionParser.parseTransitions(method, context);

        assertThat(transitions).hasSize(1);
        assertThat(transitions.getFirst().getGuards().getFirst().internalLogic())
                .contains("return expected.equals(context.getMessage());")
                .doesNotContain("protected Guard<String, String> guardVarEquals");
    }

    private MethodDeclaration createMethodDeclarationWithBindings(String source) {
        ASTParser parser = ASTParser.newParser(AST.getJLSLatest());
        parser.setKind(ASTParser.K_COMPILATION_UNIT);
        parser.setResolveBindings(true);
        parser.setBindingsRecovery(true);
        parser.setEnvironment(new String[0], new String[0], null, true);
        parser.setUnitName("TestClass.java");
        parser.setSource(source.toCharArray());
        CompilationUnit cu = (CompilationUnit) parser.createAST(null);
        if (cu.types().isEmpty()) {
            return null;
        }
        TypeDeclaration typeDecl = (TypeDeclaration) cu.types().getFirst();
        MethodDeclaration[] methods = typeDecl.getMethods();
        if (methods.length > 0) {
            return methods[0];
        }
        return null;
    }
    private MethodDeclaration createMethodDeclaration(String source) {
        ASTParser parser = ASTParser.newParser(AST.getJLSLatest());
        parser.setKind(ASTParser.K_COMPILATION_UNIT);
        parser.setSource(source.toCharArray());
        CompilationUnit cu = (CompilationUnit) parser.createAST(null);
        if (cu.types().isEmpty()) {
            return null;
        }
        TypeDeclaration typeDecl = (TypeDeclaration) cu.types().getFirst();
        MethodDeclaration[] methods = typeDecl.getMethods();
        if (methods.length > 0) {
            return methods[0];
        }
        return null;
    }

    @Test
    void shouldExtractBeanClassBodyForExternalGuard(@org.junit.jupiter.api.io.TempDir java.nio.file.Path tempDir) throws java.io.IOException {
        String guardSource = """
                package com.example;
                import org.springframework.statemachine.guard.Guard;
                import org.springframework.statemachine.StateContext;
                public class CustomGuard implements Guard<String, String> {
                    @Override
                    public boolean evaluate(StateContext<String, String> context) {
                        return "OK".equals(context.getMessage().getPayload());
                    }
                }
                """;
        String configSource = """
                package com.example;
                import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer;
                public class TestClass {
                    private CustomGuard paymentGuard;
                    public void configure(StateMachineTransitionConfigurer<String, String> transitions) throws Exception {
                        transitions.withExternal()
                            .source("CREATED")
                            .target("PAID")
                            .event("PAY")
                            .guard(paymentGuard);
                    }
                }
                """;
        java.nio.file.Files.writeString(tempDir.resolve("CustomGuard.java"), guardSource);
        java.nio.file.Files.writeString(tempDir.resolve("TestClass.java"), configSource);
        context.scan(tempDir);

        TypeDeclaration td = context.getTypeDeclaration("com.example.TestClass");
        MethodDeclaration method = context.findMethodDeclaration(td, "configure", true);

        List<Transition> transitions = AstTransitionParser.parseTransitions(method, context);

        assertThat(transitions).hasSize(1);
        assertThat(transitions.getFirst().getGuards().getFirst().internalLogic())
                .contains("return \"OK\".equals(context.getMessage().getPayload());")
                .contains("public boolean evaluate");
    }

    @Test
    void shouldExtractLocalVariableLambdaGuard() {
        String source = """
                import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer;
                import org.springframework.statemachine.guard.Guard;
                public class TestClass {
                    public void configure(StateMachineTransitionConfigurer<String, String> transitions) throws Exception {
                        Guard<String, String> myLocalGuard = context -> "LOCAL_OK".equals(context.getMessage());
                        transitions.withExternal()
                            .source("CREATED")
                            .target("PAID")
                            .event("PAY")
                            .guard(myLocalGuard);
                    }
                }
                """;
        MethodDeclaration method = createMethodDeclaration(source);

        List<Transition> transitions = AstTransitionParser.parseTransitions(method, context);

        assertThat(transitions).hasSize(1);
        assertThat(transitions.getFirst().getGuards().getFirst().internalLogic())
                .contains("context -> \"LOCAL_OK\".equals(context.getMessage())");
    }
}
