package click.kamil.springstatemachineexporter.analysis.index;

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.Nested;
import org.junit.jupiter.api.Test;

import java.util.Optional;

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

class TrivialAccessorDetectorTest {

    private final TrivialAccessorDetector detector = new TrivialAccessorDetector();

    private static final TrivialAccessorDetector.FieldLookup KNOWN_FIELDS =
            fieldName -> Optional.of("com.example.Demo");

    @Nested
    class Getters {

        @Test
        void shouldDetectPlainGetter() {
            MethodDeclaration md = method("""
                    public class Demo {
                        private String event;
                        public String getEvent() { return this.event; }
                    }
                    """, 0);

            Optional<AccessorSummary> summary = detector.detect("com.example.Demo", md, KNOWN_FIELDS);

            assertThat(summary).isPresent();
            assertThat(summary.get().kind()).isEqualTo(AccessorKind.GETTER);
            assertThat(summary.get().fieldName()).isEqualTo("event");
            assertThat(summary.get().methodName()).isEqualTo("getEvent");
        }

        @Test
        void shouldDetectGetterWithoutThisPrefix() {
            MethodDeclaration md = method("""
                    public class Demo {
                        private String event;
                        public String getEvent() { return event; }
                    }
                    """, 0);

            assertThat(detector.detect("com.example.Demo", md, KNOWN_FIELDS)).isPresent();
        }

        @Test
        void shouldDetectBooleanGetter() {
            MethodDeclaration md = method("""
                    public class Demo {
                        private boolean active;
                        public boolean isActive() { return active; }
                    }
                    """, 0);

            Optional<AccessorSummary> summary = detector.detect("com.example.Demo", md, KNOWN_FIELDS);

            assertThat(summary).isPresent();
            assertThat(summary.get().kind()).isEqualTo(AccessorKind.BOOLEAN_GETTER);
            assertThat(summary.get().fieldName()).isEqualTo("active");
        }

        @Test
        void shouldDetectGetterWithCast() {
            MethodDeclaration md = method("""
                    public class Demo {
                        private Object event;
                        public Object getEvent() { return (Object) this.event; }
                    }
                    """, 0);

            assertThat(detector.detect("com.example.Demo", md, KNOWN_FIELDS)).isPresent();
        }

        @Test
        void shouldDetectConstantGetter() {
            MethodDeclaration md = method("""
                    public class Demo {
                        public String getEvent() { return "PAY"; }
                    }
                    """, 0);

            Optional<AccessorSummary> summary = detector.detect("com.example.Demo", md, KNOWN_FIELDS);

            assertThat(summary).isPresent();
            assertThat(summary.get().kind()).isEqualTo(AccessorKind.CONSTANT_GETTER);
        }

        @Test
        void shouldDetectEnumConstantGetter() {
            MethodDeclaration md = method("""
                    public class Demo {
                        public OrderEvents getType() { return OrderEvents.PAY; }
                    }
                    enum OrderEvents { PAY }
                    """, 0);

            Optional<AccessorSummary> summary = detector.detect("com.example.Demo", md, KNOWN_FIELDS);

            assertThat(summary).isPresent();
            assertThat(summary.get().kind()).isEqualTo(AccessorKind.CONSTANT_GETTER);
        }

        @Test
        void shouldRejectComputedConstantGetter() {
            MethodDeclaration md = method("""
                    public class Demo {
                        public String getEvent() { return build(); }
                        private String build() { return "X"; }
                    }
                    """, 0);

            assertThat(detector.detect("com.example.Demo", md, KNOWN_FIELDS)).isEmpty();
        }

        @Test
        void shouldRejectLazyInitGetter() {
            MethodDeclaration md = method("""
                    public class Demo {
                        private String event;
                        public String getEvent() {
                            if (event == null) {
                                event = "default";
                            }
                            return event;
                        }
                    }
                    """, 0);

            assertThat(detector.detect("com.example.Demo", md, KNOWN_FIELDS)).isEmpty();
        }

        @Test
        void shouldRejectGetterWithMethodCall() {
            MethodDeclaration md = method("""
                    public class Demo {
                        private String event;
                        public String getEvent() { return event.trim(); }
                    }
                    """, 0);

            assertThat(detector.detect("com.example.Demo", md, KNOWN_FIELDS)).isEmpty();
        }

        @Test
        void shouldRejectUnknownField() {
            MethodDeclaration md = method("""
                    public class Demo {
                        public String getMissing() { return missing; }
                    }
                    """, 0);

            assertThat(detector.detect("com.example.Demo", md, fieldName -> Optional.empty())).isEmpty();
        }
    }

    @Nested
    class Setters {

        @Test
        void shouldDetectVoidSetter() {
            MethodDeclaration md = method("""
                    public class Demo {
                        private String event;
                        public void setEvent(String event) { this.event = event; }
                    }
                    """, 0);

            Optional<AccessorSummary> summary = detector.detect("com.example.Demo", md, KNOWN_FIELDS);

            assertThat(summary).isPresent();
            assertThat(summary.get().kind()).isEqualTo(AccessorKind.SETTER);
            assertThat(summary.get().paramName()).isEqualTo("event");
            assertThat(summary.get().returnsThis()).isFalse();
        }

        @Test
        void shouldDetectFluentSetter() {
            MethodDeclaration md = method("""
                    public class Demo {
                        private String event;
                        public Demo setEvent(String event) {
                            this.event = event;
                            return this;
                        }
                    }
                    """, 0);

            Optional<AccessorSummary> summary = detector.detect("com.example.Demo", md, KNOWN_FIELDS);

            assertThat(summary).isPresent();
            assertThat(summary.get().kind()).isEqualTo(AccessorKind.FLUENT_SETTER);
            assertThat(summary.get().returnsThis()).isTrue();
        }

        @Test
        void shouldRejectSetterWithValidation() {
            MethodDeclaration md = method("""
                    public class Demo {
                        private String event;
                        public void setEvent(String event) {
                            java.util.Objects.requireNonNull(event);
                            this.event = event;
                        }
                    }
                    """, 0);

            assertThat(detector.detect("com.example.Demo", md, KNOWN_FIELDS)).isEmpty();
        }
    }

    @Nested
    class Records {

        @Test
        void shouldDetectRecordComponentAccessor() {
            Optional<AccessorSummary> summary = detector.detectRecordComponent("com.example.Event", "code");

            assertThat(summary).isPresent();
            assertThat(summary.get().kind()).isEqualTo(AccessorKind.RECORD_COMPONENT);
            assertThat(summary.get().methodName()).isEqualTo("code");
            assertThat(summary.get().fieldName()).isEqualTo("code");
        }
    }

    @Nested
    class Naming {

        @Test
        void shouldDecapitalizeJavaBeanProperty() {
            assertThat(TrivialAccessorDetector.decapitalize("Event")).isEqualTo("event");
            assertThat(TrivialAccessorDetector.decapitalize("URL")).isEqualTo("URL");
        }
    }

    private static MethodDeclaration method(String classSource, int methodIndex) {
        ASTParser parser = ASTParser.newParser(AST.getJLSLatest());
        parser.setSource(classSource.toCharArray());
        parser.setKind(ASTParser.K_COMPILATION_UNIT);
        CompilationUnit cu = (CompilationUnit) parser.createAST(null);
        TypeDeclaration td = (TypeDeclaration) cu.types().get(0);
        return td.getMethods()[methodIndex];
    }
}
