package click.kamil.springstatemachineexporter.analysis.index;

import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
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.Optional;

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

class AccessorIndexBuilderTest {

    @Nested
    class ClassAccessors {

        @Test
        void shouldIndexTrivialGetterAndSetter(@TempDir Path tempDir) throws IOException {
            writeJava(tempDir, "com/example/Order.java", """
                    package com.example;
                    public class Order {
                        private String event;
                        public String getEvent() { return event; }
                        public void setEvent(String event) { this.event = event; }
                    }
                    """);

            AccessorIndex index = scan(tempDir);

            assertThat(index.lookup("com.example.Order", "getEvent")).isPresent();
            assertThat(index.lookup("com.example.Order", "setEvent")).isPresent();
            assertThat(index.accessorsForType("com.example.Order")).hasSize(2);
        }

        @Test
        void shouldIndexNestedClassAccessors(@TempDir Path tempDir) throws IOException {
            writeJava(tempDir, "com/example/Outer.java", """
                    package com.example;
                    public class Outer {
                        static class Inner {
                            private String code;
                            public String getCode() { return code; }
                        }
                    }
                    """);

            AccessorIndex index = scan(tempDir);

            assertThat(index.lookup("com.example.Outer.Inner", "getCode")).isPresent();
        }
    }

    @Nested
    class Inheritance {

        @Test
        void shouldInheritParentTrivialGetter(@TempDir Path tempDir) throws IOException {
            writeJava(tempDir, "com/example/Base.java", """
                    package com.example;
                    public class Base {
                        protected String event;
                        public String getEvent() { return event; }
                    }
                    """);
            writeJava(tempDir, "com/example/Child.java", """
                    package com.example;
                    public class Child extends Base {
                        public void run() {}
                    }
                    """);

            AccessorIndex index = scan(tempDir);

            assertThat(index.lookup("com.example.Child", "getEvent")).isPresent();
            assertThat(index.lookup("com.example.Child", "getEvent").orElseThrow().ownerFqn())
                    .isEqualTo("com.example.Child");
        }

        @Test
        void shouldNotInheritWhenChildOverridesWithLogic(@TempDir Path tempDir) throws IOException {
            writeJava(tempDir, "com/example/Base.java", """
                    package com.example;
                    public class Base {
                        protected String event;
                        public String getEvent() { return event; }
                    }
                    """);
            writeJava(tempDir, "com/example/Child.java", """
                    package com.example;
                    public class Child extends Base {
                        @Override
                        public String getEvent() {
                            return event == null ? "NONE" : event;
                        }
                    }
                    """);

            AccessorIndex index = scan(tempDir);

            assertThat(index.lookup("com.example.Base", "getEvent")).isPresent();
            assertThat(index.lookup("com.example.Child", "getEvent")).isEmpty();
        }

        @Test
        void shouldPreferChildTrivialOverride(@TempDir Path tempDir) throws IOException {
            writeJava(tempDir, "com/example/Base.java", """
                    package com.example;
                    public class Base {
                        protected String event;
                        public String getEvent() { return event; }
                    }
                    """);
            writeJava(tempDir, "com/example/Child.java", """
                    package com.example;
                    public class Child extends Base {
                        @Override
                        public String getEvent() { return this.event; }
                    }
                    """);

            AccessorIndex index = scan(tempDir);

            Optional<AccessorSummary> child = index.lookup("com.example.Child", "getEvent");
            assertThat(child).isPresent();
            assertThat(child.orElseThrow().ownerFqn()).isEqualTo("com.example.Child");
        }

        @Test
        void shouldResolveFieldDeclaredInSuperclass(@TempDir Path tempDir) throws IOException {
            writeJava(tempDir, "com/example/Base.java", """
                    package com.example;
                    public class Base {
                        protected String event;
                    }
                    """);
            writeJava(tempDir, "com/example/Child.java", """
                    package com.example;
                    public class Child extends Base {
                        public String getEvent() { return event; }
                    }
                    """);

            AccessorIndex index = scan(tempDir);

            AccessorSummary summary = index.lookup("com.example.Child", "getEvent").orElseThrow();
            assertThat(summary.declaringFqn()).isEqualTo("com.example.Base");
        }
    }

    @Nested
    class Records {

        @Test
        void shouldIndexRecordComponents(@TempDir Path tempDir) throws IOException {
            writeJava(tempDir, "com/example/Event.java", """
                    package com.example;
                    public record Event(String code, int version) {}
                    """);

            AccessorIndex index = scan(tempDir);

            assertThat(index.lookup("com.example.Event", "code")).isPresent();
            assertThat(index.lookup("com.example.Event", "code").orElseThrow().kind())
                    .isEqualTo(AccessorKind.RECORD_COMPONENT);
            assertThat(index.lookup("com.example.Event", "version")).isPresent();
        }
    }

    @Nested
    class Interfaces {

        @Test
        void shouldIndexDefaultInterfaceAccessor(@TempDir Path tempDir) throws IOException {
            writeJava(tempDir, "com/example/HasEvent.java", """
                    package com.example;
                    public interface HasEvent {
                        default String getEvent() {
                            return null;
                        }
                    }
                    """);

            AccessorIndex index = scan(tempDir);

            // Default method body is not trivial (returns null literal, not field read)
            assertThat(index.lookup("com.example.HasEvent", "getEvent")).isEmpty();
        }

        @Test
        void shouldIndexTrivialDefaultInterfaceAccessor(@TempDir Path tempDir) throws IOException {
            writeJava(tempDir, "com/example/Holder.java", """
                    package com.example;
                    public interface Holder {
                        String getEvent();
                    }
                    """);
            writeJava(tempDir, "com/example/Impl.java", """
                    package com.example;
                    public class Impl implements Holder {
                        private String event;
                        public String getEvent() { return event; }
                    }
                    """);

            AccessorIndex index = scan(tempDir);

            assertThat(index.lookup("com.example.Impl", "getEvent")).isPresent();
            assertThat(index.lookup("com.example.Holder", "getEvent")).isEmpty();
        }
    }

    @Nested
    class Enums {

        @Test
        void shouldIndexEnumFieldGetter(@TempDir Path tempDir) throws IOException {
            writeJava(tempDir, "com/example/Status.java", """
                    package com.example;
                    public enum Status {
                        OK;
                        private final String code = "ok";
                        public String getCode() { return code; }
                    }
                    """);

            AccessorIndex index = scan(tempDir);

            assertThat(index.lookup("com.example.Status", "getCode")).isPresent();
        }
    }

    @Test
    void shouldBuildViaCodebaseContext(@TempDir Path tempDir) throws IOException {
        writeJava(tempDir, "com/example/Demo.java", """
                package com.example;
                public class Demo {
                    private String type;
                    public String getType() { return type; }
                }
                """);

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

        assertThat(context.getAccessorIndex().lookup("com.example.Demo", "getType")).isPresent();
        assertThat(context.getAccessorIndex().trivialCount()).isGreaterThan(0);
    }

    private static AccessorIndex scan(Path tempDir) throws IOException {
        CodebaseContext context = new CodebaseContext();
        context.scan(tempDir);
        return context.getAccessorIndex();
    }

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