package click.kamil.springstatemachineexporter.ast.common;

import org.eclipse.jdt.core.dom.TypeDeclaration;
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 java.util.Set;

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

class CodebaseContextTest {

    @TempDir
    Path tempDir;

    @Test
    void shouldFindEntryPointByAnnotation() throws IOException {
        String source = """
                package com.example;
                import org.springframework.statemachine.config.EnableStateMachine;
                @EnableStateMachine
                public class MyConfig {}
                """;
        Files.writeString(tempDir.resolve("MyConfig.java"), source);

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

        List<TypeDeclaration> entryPoints = context.findEntryPointClasses(List.of("EnableStateMachine"));
        assertThat(entryPoints).hasSize(1);
        assertThat(entryPoints.get(0).getName().getIdentifier()).isEqualTo("MyConfig");
    }

    @Test
    void shouldFindEntryPointByInheritance() throws IOException {
        String baseSource = """
                package com.example;
                import org.springframework.statemachine.config.StateMachineConfigurerAdapter;
                public abstract class BaseConfig extends StateMachineConfigurerAdapter {}
                """;
        String leafSource = """
                package com.example;
                public class LeafConfig extends BaseConfig {}
                """;
        Files.writeString(tempDir.resolve("BaseConfig.java"), baseSource);
        Files.writeString(tempDir.resolve("LeafConfig.java"), leafSource);

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

        List<TypeDeclaration> entryPoints = context.findEntryPointClasses(List.of("EnableStateMachine"));
        assertThat(entryPoints).hasSize(1);
        assertThat(entryPoints.get(0).getName().getIdentifier()).isEqualTo("LeafConfig");
    }

    @Test
    void shouldNotFindAbstractClassAsEntryPoint() throws IOException {
        String source = """
                package com.example;
                import org.springframework.statemachine.config.StateMachineConfigurerAdapter;
                public abstract class MyConfig extends StateMachineConfigurerAdapter {}
                """;
        Files.writeString(tempDir.resolve("MyConfig.java"), source);

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

        List<TypeDeclaration> entryPoints = context.findEntryPointClasses(List.of("EnableStateMachine"));
        assertThat(entryPoints).isEmpty();
    }

    @Test
    void shouldResolveInheritanceWithImports() throws IOException {
        Files.createDirectories(tempDir.resolve("base"));
        Files.createDirectories(tempDir.resolve("leaf"));

        String baseSource = """
                package com.example.base;
                import org.springframework.statemachine.config.StateMachineConfigurerAdapter;
                public abstract class BaseConfig extends StateMachineConfigurerAdapter {}
                """;
        String leafSource = """
                package com.example.leaf;
                import com.example.base.BaseConfig;
                public class LeafConfig extends BaseConfig {}
                """;
        Files.writeString(tempDir.resolve("base/BaseConfig.java"), baseSource);
        Files.writeString(tempDir.resolve("leaf/LeafConfig.java"), leafSource);

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

        List<TypeDeclaration> entryPoints = context.findEntryPointClasses(List.of("EnableStateMachine"));
        assertThat(entryPoints).hasSize(1);
        assertThat(entryPoints.get(0).getName().getIdentifier()).isEqualTo("LeafConfig");
    }

    @Test
    void shouldFindBeanMethodsReturningStateMachine() throws IOException {
        String source = """
                package com.example;
                import org.springframework.context.annotation.Bean;
                import org.springframework.statemachine.StateMachine;
                public class MyApp {
                    @Bean
                    public StateMachine<String, String> stateMachine() {
                        return null;
                    }
                
                    public StateMachine<String, String> notABean() {
                        return null;
                    }
                }
                """;
        Files.writeString(tempDir.resolve("MyApp.java"), source);

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

        var beanMethods = context.findBeanMethodsReturning(java.util.Set.of("StateMachine"));
        assertThat(beanMethods).hasSize(1);
        assertThat(beanMethods.get(0).getName().getIdentifier()).isEqualTo("stateMachine");
    }

    @Test
    void shouldFindBeanMethodsReturningStateMachineWithQualifiedName() throws IOException {
        String source = """
                package com.example;
                import org.springframework.context.annotation.Bean;
                public class MyApp {
                    @Bean
                    public org.springframework.statemachine.StateMachine stateMachine() {
                        return null;
                    }
                }
                """;
        Files.writeString(tempDir.resolve("MyApp.java"), source);

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

        var beanMethods = context.findBeanMethodsReturning(java.util.Set.of("StateMachine"));
        assertThat(beanMethods).hasSize(1);
        assertThat(beanMethods.get(0).getName().getIdentifier()).isEqualTo("stateMachine");
    }

    @Test
    void shouldFindClassWithMultipleAnnotations() throws IOException {
        String source = """
                package com.example;
                @org.springframework.context.annotation.Configuration
                @click.kamil.MyAnnotation
                public class MultiAnnotated {}
                """;
        Files.writeString(tempDir.resolve("MultiAnnotated.java"), source);

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

        assertThat(context.findEntryPointClasses(List.of("Configuration"))).hasSize(1);
        assertThat(context.findEntryPointClasses(List.of("MyAnnotation"))).hasSize(1);
    }

    @Test
    void shouldIgnoreInterfacesWithAnnotations() throws IOException {
        String source = """
                package com.example;
                import org.springframework.context.annotation.Configuration;
                @Configuration
                public interface MyInterface {}
                """;
        Files.writeString(tempDir.resolve("MyInterface.java"), source);

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

        assertThat(context.findEntryPointClasses(List.of("Configuration"))).isEmpty();
    }

    @Test
    void shouldFindBeanMethodsWithGenericParameters() throws IOException {
        String source = """
                package com.example;
                import org.springframework.context.annotation.Bean;
                import org.springframework.statemachine.StateMachine;
                public class MyApp {
                    @Bean
                    public StateMachine<States, Events> stateMachine() {
                        return null;
                    }
                }
                """;
        Files.writeString(tempDir.resolve("MyApp.java"), source);

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

        var beanMethods = context.findBeanMethodsReturning(java.util.Set.of("StateMachine"));
        assertThat(beanMethods).hasSize(1);
    }

    @Test
    void shouldHandleFilesWithOnlyComments() throws IOException {
        String source = """
                // This is just a comment
                /* And another one */
                """;
        Files.writeString(tempDir.resolve("Comments.java"), source);

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

        assertThat(context.findEntryPointClasses(List.of("EnableStateMachine"))).isEmpty();
    }

    @Test
    void shouldBeCaseSensitiveForAnnotations() throws IOException {
        String source = """
                package com.example;
                @enablestatemachine
                public class WrongCase {}
                """;
        Files.writeString(tempDir.resolve("WrongCase.java"), source);

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

        assertThat(context.findEntryPointClasses(List.of("EnableStateMachine"))).isEmpty();
    }

    @Test
    void getEnumValuesShouldFailClosedForAmbiguousSimpleEnumName() throws IOException {
        Files.createDirectories(tempDir.resolve("a"));
        Files.createDirectories(tempDir.resolve("b"));
        Files.writeString(tempDir.resolve("a/OrderEvent.java"),
                "package a; public enum OrderEvent { PAY }");
        Files.writeString(tempDir.resolve("b/OrderEvent.java"),
                "package b; public enum OrderEvent { SHIP }");

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

        assertThat(context.getEnumValues("OrderEvent"))
                .as("ambiguous simple enum name must not resolve to arbitrary package")
                .isNull();
        assertThat(context.getEnumValues("a.OrderEvent")).containsExactly("a.OrderEvent.PAY");
        assertThat(context.getEnumValues("b.OrderEvent")).containsExactly("b.OrderEvent.SHIP");
        assertThat(context.isAmbiguousSimpleName("OrderEvent")).isTrue();
        assertThat(context.isAmbiguousSimpleName("a.OrderEvent")).isFalse();
    }

    @Test
    void getImplementationsShouldFailClosedForAmbiguousSimpleTypeName() throws IOException {
        Files.createDirectories(tempDir.resolve("a"));
        Files.createDirectories(tempDir.resolve("b"));
        Files.writeString(tempDir.resolve("a/Service.java"),
                "package a; public interface Service {}");
        Files.writeString(tempDir.resolve("a/AServiceImpl.java"),
                "package a; public class AServiceImpl implements Service {}");
        Files.writeString(tempDir.resolve("b/Service.java"),
                "package b; public interface Service {}");
        Files.writeString(tempDir.resolve("b/BServiceImpl.java"),
                "package b; public class BServiceImpl implements Service {}");

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

        assertThat(context.getImplementations("Service"))
                .as("ambiguous simple interface name must not widen to arbitrary impls")
                .isEmpty();
        assertThat(context.getImplementations("a.Service")).containsExactly("a.AServiceImpl");
        assertThat(context.getImplementations("b.Service")).containsExactly("b.BServiceImpl");
    }

    @Test
    void getImplementationsShouldMergeFqnAndSimpleNameIndexBuckets() throws IOException {
        Files.createDirectories(tempDir.resolve("abcd/ccc"));
        Files.writeString(tempDir.resolve("abcd/ccc/AML.java"), """
                package abcd.ccc;
                public abstract class AML {}
                """);
        Files.writeString(tempDir.resolve("abcd/ccc/RealListener.java"), """
                package abcd.ccc;
                public class RealListener extends AML {}
                """);
        // KAPT-style stub: no import, FQN in extends clause
        Files.writeString(tempDir.resolve("abcd/ccc/Av.java"), """
                package abcd.ccc;
                public class Av extends abcd.ccc.AML {}
                """);

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

        assertThat(context.getImplementations("abcd.ccc.AML"))
                .containsExactlyInAnyOrder("abcd.ccc.Av", "abcd.ccc.RealListener");
        assertThat(context.getImplementations("AML"))
                .containsExactlyInAnyOrder("abcd.ccc.Av", "abcd.ccc.RealListener");
    }

    @Test
    void getImplementationsShouldResolveWhenSubclassScannedBeforeSuperclass() throws IOException {
        Files.createDirectories(tempDir.resolve("abcd/ccc"));
        // Names sort before AML so Files.walk typically indexes subclasses first.
        Files.writeString(tempDir.resolve("abcd/ccc/AaaListener.java"), """
                package abcd.ccc;
                public class AaaListener extends AML {}
                """);
        Files.writeString(tempDir.resolve("abcd/ccc/Av.java"), """
                package abcd.ccc;
                public class Av extends abcd.ccc.AML {}
                """);
        Files.writeString(tempDir.resolve("abcd/ccc/AML.java"), """
                package abcd.ccc;
                public abstract class AML {}
                """);

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

        assertThat(context.getImplementations("abcd.ccc.AML"))
                .containsExactlyInAnyOrder("abcd.ccc.AaaListener", "abcd.ccc.Av");
        assertThat(context.getImplementations("AML"))
                .containsExactlyInAnyOrder("abcd.ccc.AaaListener", "abcd.ccc.Av");
    }

    @Test
    void getImplementationsShouldTerminateOnCircularExtends() throws IOException {
        Files.createDirectories(tempDir.resolve("cycle"));
        // Invalid Java inheritance cycle — still must not hang in collectImplementations / subtype walks.
        Files.writeString(tempDir.resolve("cycle/A.java"), """
                package cycle;
                public class A extends B {}
                """);
        Files.writeString(tempDir.resolve("cycle/B.java"), """
                package cycle;
                public class B extends A {}
                """);

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

        assertThat(context.getImplementations("cycle.A")).contains("cycle.B");
        assertThat(context.getImplementations("cycle.B")).contains("cycle.A");
        assertThat(context.isSubtypeOrSame("cycle.A", "cycle.B")).isTrue();
        assertThat(context.isSubtypeOrSame("cycle.B", "cycle.A")).isTrue();
        assertThat(context.areClassesPolymorphicallyCompatible("cycle.A", "cycle.B")).isTrue();
    }

    @Test
    void forEachSuperclassShouldVisitEachTypeOnceOnCircularExtends() throws IOException {
        Files.createDirectories(tempDir.resolve("cycle"));
        Files.writeString(tempDir.resolve("cycle/A.java"), """
                package cycle;
                public class A extends B {}
                """);
        Files.writeString(tempDir.resolve("cycle/B.java"), """
                package cycle;
                public class B extends A {}
                """);

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

        List<String> visited = new ArrayList<>();
        TypeDeclaration a = context.getTypeDeclaration("cycle.A");
        context.forEachSuperclass(a, td -> visited.add(context.getFqn(td)));

        assertThat(visited).containsExactly("cycle.A", "cycle.B");
    }

    @Test
    void findMethodDeclarationShouldTerminateOnCircularExtends() throws IOException {
        Files.createDirectories(tempDir.resolve("cycle"));
        Files.writeString(tempDir.resolve("cycle/Base.java"), """
                package cycle;
                public class Base {
                    public void handle() {}
                }
                """);
        Files.writeString(tempDir.resolve("cycle/A.java"), """
                package cycle;
                public class A extends B {}
                """);
        Files.writeString(tempDir.resolve("cycle/B.java"), """
                package cycle;
                public class B extends A {}
                """);

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

        TypeDeclaration a = context.getTypeDeclaration("cycle.A");
        assertThat(context.findMethodDeclaration(a, "handle", true)).isNull();
        assertThat(context.findMethodDeclaration(a, "missing", true)).isNull();
    }

    @Test
    void indexShouldPreferMainSourceOverGeneratedStubForSameFqn() throws IOException {
        Path module = tempDir.resolve("module");
        Path mainFoo = module.resolve("src/main/java/com/example/Foo.java");
        Path stubFoo = module.resolve("target/generated-sources/annotations/com/example/Foo.java");
        Files.createDirectories(mainFoo.getParent());
        Files.createDirectories(stubFoo.getParent());
        Files.writeString(mainFoo, """
                package com.example;
                public class Foo {
                    public String realBody() { return "main"; }
                }
                """);
        Files.writeString(stubFoo, """
                package com.example;
                public class Foo {
                    public String stubBody() { return "stub"; }
                }
                """);

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

        assertThat(context.getPath("com.example.Foo").toString()).contains("src/main/java");
        assertThat(context.getTypeDeclaration("com.example.Foo").getMethods())
                .extracting(md -> md.getName().getIdentifier())
                .contains("realBody")
                .doesNotContain("stubBody");
    }

    @Test
    void indexShouldPreferMainSourceOverGeneratedStubRegardlessOfScanRootOrder() throws IOException {
        Path module = tempDir.resolve("module");
        Path mainFoo = module.resolve("src/main/java/com/example/Foo.java");
        Path stubFoo = module.resolve("target/generated-sources/annotations/com/example/Foo.java");
        Files.createDirectories(mainFoo.getParent());
        Files.createDirectories(stubFoo.getParent());
        Files.writeString(mainFoo, """
                package com.example;
                public class Foo { public void realBody() {} }
                """);
        Files.writeString(stubFoo, """
                package com.example;
                public class Foo { public void stubBody() {} }
                """);

        CodebaseContext generatedFirst = new CodebaseContext();
        generatedFirst.scan(Set.of(
                module.resolve("target/generated-sources"),
                module.resolve("src/main/java")), Set.of());

        CodebaseContext mainFirst = new CodebaseContext();
        mainFirst.scan(Set.of(
                module.resolve("src/main/java"),
                module.resolve("target/generated-sources")), Set.of());

        for (CodebaseContext context : List.of(generatedFirst, mainFirst)) {
            assertThat(context.getPath("com.example.Foo").toString()).contains("src/main/java");
            assertThat(context.getTypeDeclaration("com.example.Foo").getMethods())
                    .extracting(md -> md.getName().getIdentifier())
                    .contains("realBody")
                    .doesNotContain("stubBody");
        }
    }

    @Test
    void getImplementationsShouldCanonicalizeImportAwareExtendsToSameFqnBucket() throws IOException {
        Files.createDirectories(tempDir.resolve("abcd/ccc"));
        Files.createDirectories(tempDir.resolve("other/pkg"));
        Files.writeString(tempDir.resolve("abcd/ccc/AML.java"), """
                package abcd.ccc;
                public abstract class AML {}
                """);
        Files.writeString(tempDir.resolve("abcd/ccc/SamePackageListener.java"), """
                package abcd.ccc;
                public class SamePackageListener extends AML {}
                """);
        Files.writeString(tempDir.resolve("other/pkg/ImportedListener.java"), """
                package other.pkg;
                import abcd.ccc.AML;
                public class ImportedListener extends AML {}
                """);

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

        assertThat(context.getImplementations("abcd.ccc.AML"))
                .containsExactlyInAnyOrder("abcd.ccc.SamePackageListener", "other.pkg.ImportedListener");
    }

    @Test
    void areSameTypeOrUnambiguousSimpleMatchShouldFailClosedForAmbiguousTypes() throws IOException {
        Files.createDirectories(tempDir.resolve("a"));
        Files.createDirectories(tempDir.resolve("b"));
        Files.writeString(tempDir.resolve("a/Dispatcher.java"),
                "package a; public class Dispatcher { public void dispatch() {} }");
        Files.writeString(tempDir.resolve("b/Dispatcher.java"),
                "package b; public class Dispatcher { public void dispatch() {} }");

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

        assertThat(context.areSameTypeOrUnambiguousSimpleMatch("a.Dispatcher", "b.Dispatcher")).isFalse();
        assertThat(context.areClassesPolymorphicallyCompatible("a.Dispatcher", "b.Dispatcher")).isFalse();
    }

    @Test
    void getSuperclassFqnShouldPreferImportAwareResolutionWhenBindingMisResolvesPackage() throws IOException {
        Files.createDirectories(tempDir.resolve("com/example/routes"));
        Files.writeString(tempDir.resolve("com/example/routes/BaseRoutes.java"), """
                package com.example.routes;
                public abstract class BaseRoutes {}
                """);
        Files.writeString(tempDir.resolve("com/example/App.java"), """
                package com.example;
                import com.example.routes.BaseRoutes;
                class OrderGateway extends BaseRoutes {}
                """);

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

        assertThat(context.getTypeDeclaration("com.example.routes.BaseRoutes")).isNotNull();
        assertThat(context.getSuperclassFqn(context.getTypeDeclaration("com.example.OrderGateway")))
                .isEqualTo("com.example.routes.BaseRoutes");
    }

    @Test
    void shouldFindClassWithFullyQualifiedAnnotation() throws IOException {
        String source = """
                package com.example;
                @org.springframework.statemachine.config.EnableStateMachine
                public class FqnAnnotated {}
                """;
        Files.writeString(tempDir.resolve("FqnAnnotated.java"), source);

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

        assertThat(context.findEntryPointClasses(List.of("EnableStateMachine"))).hasSize(1);
    }

    @Test
    void isSubtypeOrSameShouldWalkSuperclassAndInterfaces() throws IOException {
        Files.writeString(tempDir.resolve("Hierarchy.java"), """
                package com.example;
                interface Named {}
                abstract class AbstractListener implements Named {}
                class OrderListener extends AbstractListener {}
                class DocumentListener extends AbstractListener {}
                class SpecialOrderListener extends OrderListener {}
                """);

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

        assertThat(context.isSubtypeOrSame("com.example.OrderListener", "com.example.OrderListener")).isTrue();
        assertThat(context.isSubtypeOrSame("com.example.OrderListener", "com.example.AbstractListener")).isTrue();
        assertThat(context.isSubtypeOrSame("com.example.OrderListener", "com.example.Named")).isTrue();
        assertThat(context.isSubtypeOrSame("com.example.SpecialOrderListener", "com.example.OrderListener")).isTrue();
        assertThat(context.isSubtypeOrSame("com.example.OrderListener", "com.example.DocumentListener")).isFalse();
        assertThat(context.isSubtypeOrSame("com.example.DocumentListener", "com.example.OrderListener")).isFalse();
        assertThat(context.isConcreteClassType("com.example.OrderListener")).isTrue();
        assertThat(context.isConcreteClassType("com.example.AbstractListener")).isFalse();
        assertThat(context.isConcreteClassType("com.example.Named")).isFalse();
    }
}
