package click.kamil.springstatemachineexporter.analysis.resolver;

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.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

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

class StaticBuildFileAnalyzerTest {

    private final StaticBuildFileAnalyzer analyzer = new StaticBuildFileAnalyzer();

    @Nested
    class GradleIncludes {

        @Test
        void shouldParseSettingsGradleIncludeList(@TempDir Path tempDir) throws IOException {
            Path root = tempDir.resolve("monorepo");
            Files.createDirectories(root);
            Files.writeString(root.resolve("settings.gradle"), """
                    rootProject.name = 'monorepo'
                    include 'module-a', 'module-b', 'module-c'
                    """);
            createGradleModule(root, "module-a");
            createGradleModule(root, "module-b");
            createGradleModule(root, "module-c");

            Map<String, Path> includes = analyzer.parseGradleIncludes(root);

            assertThat(includes).hasSize(3);
            assertThat(includes.get("module-a")).isEqualTo(root.resolve("module-a").normalize());
            assertThat(includes.get("module-b")).isEqualTo(root.resolve("module-b").normalize());
            assertThat(includes.get("module-c")).isEqualTo(root.resolve("module-c").normalize());
        }

        @Test
        void shouldParseSettingsGradleKtsIncludeCall(@TempDir Path tempDir) throws IOException {
            Path root = tempDir.resolve("monorepo");
            Files.createDirectories(root);
            Files.writeString(root.resolve("settings.gradle.kts"), """
                    include(":api", ":core")
                    """);
            createGradleModule(root, "api");
            createGradleModule(root, "core");

            Map<String, Path> includes = analyzer.parseGradleIncludes(root);

            assertThat(includes).containsKeys("api", "core");
            assertThat(includes.get("api")).isEqualTo(root.resolve("api").normalize());
        }

        @Test
        void shouldResolveNestedIncludePaths(@TempDir Path tempDir) throws IOException {
            Path root = tempDir.resolve("monorepo");
            Path nested = root.resolve("state_machines/multi/api");
            Files.createDirectories(nested);
            Files.writeString(root.resolve("settings.gradle"), """
                    include ':state_machines:multi:api', ':state_machines:multi:core'
                    """);
            createGradleModule(root, "state_machines/multi/api");
            createGradleModule(root, "state_machines/multi/core");

            Map<String, Path> includes = analyzer.parseGradleIncludes(root);

            assertThat(includes.get("state_machines:multi:api"))
                    .isEqualTo(root.resolve("state_machines/multi/api").normalize());
            assertThat(includes.get("state_machines:multi:core"))
                    .isEqualTo(root.resolve("state_machines/multi/core").normalize());
        }

        @Test
        void shouldHonorCustomProjectDirMapping(@TempDir Path tempDir) throws IOException {
            Path root = tempDir.resolve("monorepo");
            Path custom = root.resolve("custom-location/service");
            Files.createDirectories(custom);
            Files.writeString(root.resolve("settings.gradle"), """
                    include ':service'
                    project(':service').projectDir = file('custom-location/service')
                    """);
            createGradleModule(custom, ".");

            Map<String, Path> includes = analyzer.parseGradleIncludes(root);

            assertThat(includes.get("service")).isEqualTo(custom.normalize());
        }

        @Test
        void shouldDiscoverAllIncludedModulesWhenScanningFromRoot(@TempDir Path tempDir) throws IOException {
            Path root = writeGradleMonorepo(tempDir);

            ProjectModuleGraph graph = analyzer.analyze(root.resolve("module-a"));
            Set<Path> scanPaths = graph.resolveScanPaths(root);

            assertThat(scanPaths).containsExactlyInAnyOrder(
                    root.normalize(),
                    root.resolve("module-a").normalize(),
                    root.resolve("module-b").normalize(),
                    root.resolve("module-c").normalize());
        }
    }

    @Nested
    class GradleDependencyGraph {

        @Test
        void shouldBuildTransitiveProjectDependencyGraph(@TempDir Path tempDir) throws IOException {
            Path root = writeGradleMonorepo(tempDir);

            ProjectModuleGraph graph = analyzer.analyze(root.resolve("module-a"));

            List<ProjectModuleGraph.ModuleDependency> localDeps = graph.dependencies().stream()
                    .filter(dep -> dep.kind() == ProjectModuleGraph.DependencyKind.LOCAL_PROJECT)
                    .toList();

            assertThat(localDeps).anyMatch(dep ->
                    dep.fromModuleId().contains("module-a") && dep.toModuleId().contains("module-b"));
            assertThat(localDeps).anyMatch(dep ->
                    dep.fromModuleId().contains("module-b") && dep.toModuleId().contains("module-c"));

            Set<Path> siblings = new SiblingDependencyResolver().resolveSiblings(graph, root.resolve("module-a"));
            assertThat(siblings).containsExactlyInAnyOrder(
                    root.resolve("module-b").normalize(),
                    root.resolve("module-c").normalize());
        }

        @Test
        void shouldParseBuildGradleKtsProjectDependencies(@TempDir Path tempDir) throws IOException {
            Path root = tempDir.resolve("monorepo");
            Files.createDirectories(root);
            Files.createDirectories(root.resolve(".git"));
            Files.writeString(root.resolve("settings.gradle.kts"), "include(\":app\", \":lib\")");
            Files.createDirectories(root.resolve("app"));
            Files.writeString(root.resolve("app/build.gradle.kts"), """
                    dependencies {
                        implementation(project(":lib"))
                    }
                    """);
            Files.createDirectories(root.resolve("lib"));
            Files.writeString(root.resolve("lib/build.gradle.kts"), "");

            ProjectModuleGraph graph = analyzer.analyze(root.resolve("app"));
            boolean hasLibDep = graph.dependencies().stream()
                    .anyMatch(dep -> dep.kind() == ProjectModuleGraph.DependencyKind.LOCAL_PROJECT
                            && dep.fromModuleId().contains("app")
                            && dep.toModuleId().contains("lib"));
            assertThat(hasLibDep).isTrue();
        }

        @Test
        void shouldIgnoreTestImplementationProjectDependencies(@TempDir Path tempDir) throws IOException {
            Path root = tempDir.resolve("monorepo");
            Files.createDirectories(root.resolve(".git"));
            Files.writeString(root.resolve("settings.gradle"), "include 'app', 'lib', 'test-lib'");
            createGradleModule(root, "app");
            createGradleModule(root, "lib");
            createGradleModule(root, "test-lib");
            Files.writeString(root.resolve("app/build.gradle"), """
                    dependencies {
                        implementation project(':lib')
                        testImplementation project(':test-lib')
                    }
                    """);

            ProjectModuleGraph graph = analyzer.analyze(root.resolve("app"));
            List<ProjectModuleGraph.ModuleDependency> localDeps = graph.dependencies().stream()
                    .filter(dep -> dep.kind() == ProjectModuleGraph.DependencyKind.LOCAL_PROJECT)
                    .toList();

            assertThat(localDeps).anyMatch(dep -> dep.toModuleId().contains("lib"));
            assertThat(localDeps).noneMatch(dep -> dep.toModuleId().contains("test-lib"));
        }
    }

    @Nested
    class MavenCoordinates {

        @Test
        void shouldIndexModulesByGroupIdAndArtifactId(@TempDir Path tempDir) throws IOException {
            Path root = writeMavenMultiModule(tempDir);

            ProjectModuleGraph graph = analyzer.analyze(root.resolve("module-a"));

            assertThat(graph.modulesByCoordinates()).containsKeys(
                    new ProjectModuleGraph.MavenCoordinates("com.acme", "module-a"),
                    new ProjectModuleGraph.MavenCoordinates("com.acme", "module-b"),
                    new ProjectModuleGraph.MavenCoordinates("com.acme", "module-c"));
        }

        @Test
        void shouldDistinguishSameArtifactIdUnderDifferentGroups(@TempDir Path tempDir) throws IOException {
            Path root = tempDir.resolve("repo");
            Files.createDirectories(root.resolve(".git"));
            writePom(root.resolve("pom.xml"), """
                    <project>
                      <groupId>com.repo</groupId>
                      <artifactId>parent</artifactId>
                      <packaging>pom</packaging>
                      <modules>
                        <module>team-a/api</module>
                        <module>team-b/api</module>
                        <module>consumer</module>
                      </modules>
                    </project>
                    """);
            writePom(root.resolve("team-a/api/pom.xml"), """
                    <project>
                      <groupId>com.team.a</groupId>
                      <artifactId>api</artifactId>
                    </project>
                    """);
            writePom(root.resolve("team-b/api/pom.xml"), """
                    <project>
                      <groupId>com.team.b</groupId>
                      <artifactId>api</artifactId>
                    </project>
                    """);
            writePom(root.resolve("consumer/pom.xml"), """
                    <project>
                      <groupId>com.consumer</groupId>
                      <artifactId>consumer</artifactId>
                      <dependencies>
                        <dependency>
                          <groupId>com.team.a</groupId>
                          <artifactId>api</artifactId>
                          <version>1.0</version>
                        </dependency>
                      </dependencies>
                    </project>
                    """);

            ProjectModuleGraph graph = analyzer.analyze(root.resolve("consumer"));

            List<ProjectModuleGraph.ModuleDependency> deps = graph.localDependenciesOf(
                    graph.modulesByCoordinates()
                            .get(new ProjectModuleGraph.MavenCoordinates("com.consumer", "consumer"))
                            .id());

            assertThat(deps).hasSize(1);
            assertThat(deps.get(0).toModuleId()).isEqualTo(
                    graph.modulesByCoordinates().get(new ProjectModuleGraph.MavenCoordinates("com.team.a", "api")).id());
        }

        @Test
        void shouldMarkExternalLibraryWhenCoordinatesAreNotInRepo(@TempDir Path tempDir) throws IOException {
            Path root = tempDir.resolve("repo");
            Files.createDirectories(root.resolve(".git"));
            writePom(root.resolve("service/pom.xml"), """
                    <project>
                      <groupId>com.acme</groupId>
                      <artifactId>service</artifactId>
                      <dependencies>
                        <dependency>
                          <groupId>org.springframework.boot</groupId>
                          <artifactId>spring-boot-starter</artifactId>
                          <version>3.2.0</version>
                        </dependency>
                      </dependencies>
                    </project>
                    """);

            ProjectModuleGraph graph = analyzer.analyze(root.resolve("service"));
            String serviceId = graph.modulesByCoordinates()
                    .get(new ProjectModuleGraph.MavenCoordinates("com.acme", "service")).id();

            assertThat(graph.dependencies()).anyMatch(dep ->
                    dep.fromModuleId().equals(serviceId)
                            && dep.kind() == ProjectModuleGraph.DependencyKind.EXTERNAL_LIBRARY
                            && dep.externalCoordinates().key().equals("org.springframework.boot:spring-boot-starter"));
        }

        @Test
        void shouldIgnoreTestScopedMavenDependencies(@TempDir Path tempDir) throws IOException {
            Path root = tempDir.resolve("repo");
            Files.createDirectories(root.resolve(".git"));
            writePom(root.resolve("pom.xml"), """
                    <project>
                      <groupId>com.repo</groupId>
                      <artifactId>parent</artifactId>
                      <packaging>pom</packaging>
                      <modules><module>app</module><module>test-support</module></modules>
                    </project>
                    """);
            writePom(root.resolve("app/pom.xml"), """
                    <project>
                      <groupId>com.repo</groupId>
                      <artifactId>app</artifactId>
                      <dependencies>
                        <dependency>
                          <groupId>com.repo</groupId>
                          <artifactId>test-support</artifactId>
                          <version>1.0</version>
                          <scope>test</scope>
                        </dependency>
                      </dependencies>
                    </project>
                    """);
            writePom(root.resolve("test-support/pom.xml"), """
                    <project><groupId>com.repo</groupId><artifactId>test-support</artifactId></project>
                    """);

            ProjectModuleGraph graph = analyzer.analyze(root.resolve("app"));
            String appId = graph.modulesByCoordinates()
                    .get(new ProjectModuleGraph.MavenCoordinates("com.repo", "app")).id();

            assertThat(graph.localDependenciesOf(appId)).isEmpty();
        }
    }

    @Nested
    class MavenModules {

        @Test
        void shouldDiscoverAggregatorModules(@TempDir Path tempDir) throws IOException {
            Path root = tempDir.resolve("aggregator");
            Files.createDirectories(root.resolve(".git"));
            writePom(root.resolve("pom.xml"), """
                    <project>
                      <groupId>com.acme</groupId>
                      <artifactId>root</artifactId>
                      <packaging>pom</packaging>
                      <modules>
                        <module>service-x</module>
                        <module>service-y</module>
                      </modules>
                    </project>
                    """);
            writePom(root.resolve("service-x/pom.xml"), """
                    <project><groupId>com.acme</groupId><artifactId>service-x</artifactId></project>
                    """);
            writePom(root.resolve("service-y/pom.xml"), """
                    <project><groupId>com.acme</groupId><artifactId>service-y</artifactId></project>
                    """);

            ProjectModuleGraph graph = analyzer.analyze(root);
            Set<Path> siblings = new SiblingDependencyResolver().resolveSiblings(graph, root);

            assertThat(siblings).containsExactlyInAnyOrder(
                    root.resolve("service-x").normalize(),
                    root.resolve("service-y").normalize());
        }

        @Test
        void shouldResolveTransitiveMavenModuleDependencies(@TempDir Path tempDir) throws IOException {
            Path root = writeMavenMultiModule(tempDir);

            Set<Path> siblings = new SiblingDependencyResolver().resolveSiblings(root.resolve("module-a"));

            assertThat(siblings).containsExactlyInAnyOrder(
                    root.resolve("module-b").normalize(),
                    root.resolve("module-c").normalize(),
                    root.normalize());
        }

        @Test
        void shouldResolveParentViaRelativePath(@TempDir Path tempDir) throws IOException {
            Path root = tempDir.resolve("root");
            Path child = tempDir.resolve("child");
            Files.createDirectories(root);
            Files.createDirectories(child);
            writePom(root.resolve("pom.xml"), """
                    <project><groupId>com.acme</groupId><artifactId>root</artifactId></project>
                    """);
            writePom(child.resolve("pom.xml"), """
                    <project>
                      <parent>
                        <relativePath>../root/pom.xml</relativePath>
                      </parent>
                      <artifactId>child</artifactId>
                    </project>
                    """);

            Set<Path> scanPaths = analyzer.analyze(child).resolveScanPaths(child);

            assertThat(scanPaths).contains(root.toAbsolutePath().normalize());
        }

        @Test
        void shouldPreferNestedMavenAggregatorOverOuterGradleRoot(@TempDir Path tempDir) throws IOException {
            Path outerGradle = tempDir.resolve("outer");
            Path nestedMaven = outerGradle.resolve("nested-maven");
            Files.createDirectories(outerGradle.resolve(".git"));
            Files.writeString(outerGradle.resolve("settings.gradle"), "rootProject.name = 'outer'");
            writePom(nestedMaven.resolve("pom.xml"), """
                    <project>
                      <groupId>com.nested</groupId>
                      <artifactId>parent</artifactId>
                      <packaging>pom</packaging>
                      <modules><module>api</module></modules>
                    </project>
                    """);
            writePom(nestedMaven.resolve("api/pom.xml"), """
                    <project><groupId>com.nested</groupId><artifactId>api</artifactId></project>
                    """);

            ProjectModuleGraph graph = analyzer.analyze(nestedMaven.resolve("api"));

            assertThat(graph.workspaceRoot()).isEqualTo(nestedMaven.normalize());
            assertThat(graph.allModuleDirectories()).containsExactlyInAnyOrder(
                    nestedMaven.resolve("api").normalize(),
                    nestedMaven.normalize());
        }
    }

    @Nested
    class ExtractHelpers {

        @Test
        void shouldExtractGradleIncludeTokensFromMixedSyntax() {
            String settings = """
                    pluginManagement {}
                    include ':api'
                    include(":core", ":infra")
                    include 'web', 'worker'
                    """;

            assertThat(StaticBuildFileAnalyzer.extractGradleIncludePaths(settings))
                    .containsExactlyInAnyOrder("api", "core", "infra", "web", "worker");
        }

        @Test
        void shouldExtractOwnMavenCoordinatesSkippingParentSection() {
            String pom = """
                    <project>
                      <parent>
                        <groupId>com.parent</groupId>
                        <artifactId>parent</artifactId>
                      </parent>
                      <artifactId>child</artifactId>
                      <groupId>com.child</groupId>
                    </project>
                    """;

            assertThat(StaticBuildFileAnalyzer.extractOwnMavenCoordinates(pom))
                    .contains(new ProjectModuleGraph.MavenCoordinates("com.child", "child"));
        }
    }

    private static Path writeGradleMonorepo(Path tempDir) throws IOException {
        Path root = tempDir.resolve("my-root-project");
        Files.createDirectories(root);
        Files.createDirectories(root.resolve(".git"));
        Files.writeString(root.resolve("settings.gradle"), "include 'module-a', 'module-b', 'module-c'");
        createGradleModule(root, "module-a");
        createGradleModule(root, "module-b");
        createGradleModule(root, "module-c");
        Files.writeString(root.resolve("module-a/build.gradle"), "dependencies { implementation project(':module-b') }");
        Files.writeString(root.resolve("module-b/build.gradle"), "dependencies { implementation project(':module-c') }");
        return root;
    }

    private static Path writeMavenMultiModule(Path tempDir) throws IOException {
        Path root = tempDir.resolve("my-maven-root");
        Files.createDirectories(root.resolve(".git"));
        writePom(root.resolve("pom.xml"), """
                <project><groupId>com.acme</groupId><artifactId>root</artifactId></project>
                """);
        writePom(root.resolve("module-a/pom.xml"), """
                <project>
                  <groupId>com.acme</groupId>
                  <artifactId>module-a</artifactId>
                  <dependencies>
                    <dependency>
                      <groupId>com.acme</groupId>
                      <artifactId>module-b</artifactId>
                      <version>1.0</version>
                    </dependency>
                  </dependencies>
                </project>
                """);
        writePom(root.resolve("module-b/pom.xml"), """
                <project>
                  <groupId>com.acme</groupId>
                  <artifactId>module-b</artifactId>
                  <dependencies>
                    <dependency>
                      <groupId>com.acme</groupId>
                      <artifactId>module-c</artifactId>
                      <version>1.0</version>
                    </dependency>
                  </dependencies>
                </project>
                """);
        writePom(root.resolve("module-c/pom.xml"), """
                <project><groupId>com.acme</groupId><artifactId>module-c</artifactId></project>
                """);
        return root;
    }

    private static void createGradleModule(Path root, String modulePath) throws IOException {
        Path moduleDir = ".".equals(modulePath) ? root : root.resolve(modulePath);
        Files.createDirectories(moduleDir);
        Path buildFile = moduleDir.resolve("build.gradle");
        if (!Files.exists(buildFile)) {
            Files.writeString(buildFile, "");
        }
    }

    private static void writePom(Path pomFile, String content) throws IOException {
        Files.createDirectories(pomFile.getParent());
        Files.writeString(pomFile, content);
    }
}
