package p.studio.compiler.pbs;

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import p.studio.compiler.messages.BuildingIssueSink;
import p.studio.compiler.messages.FrontendPhaseContext;
import p.studio.compiler.models.BuildStack;
import p.studio.compiler.models.ProjectDescriptor;
import p.studio.compiler.models.SourceHandle;
import p.studio.compiler.models.SourceKind;
import p.studio.compiler.services.PBSFrontendPhaseService;
import p.studio.compiler.source.diagnostics.DiagnosticSink;
import p.studio.compiler.source.identifiers.ProjectId;
import p.studio.compiler.source.tables.FileTable;
import p.studio.compiler.source.tables.ProjectTable;
import p.studio.compiler.utilities.SourceProviderFactory;
import p.studio.utilities.logs.LogAggregator;
import p.studio.utilities.structures.ReadOnlyList;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static org.junit.jupiter.api.Assertions.*;

class PbsGateUStdlibCompileTest {

    @ParameterizedTest(name = "{0}")
    @MethodSource("gateCases")
    void gateU_shouldCompileSelectedStdlibVersionWithoutErrors(
            final GateCase gateCase) throws Exception {
        compileStdlib(gateCase.stdlibVersion(), gateCase.flags());
    }

    private static Stream<GateCase> gateCases() {
        return Stream.of(
                new GateCase("stdlib-v1-errors-only", 1, GateFlags.errorsOnly())
        );
    }

    private void compileStdlib(
            final int stdlibVersion,
            final GateFlags flags) throws Exception {
        final var stdlibProjects = discoverStdlibProjects(stdlibVersion);
        assertFalse(stdlibProjects.isEmpty(), "No stdlib modules were found for version " + stdlibVersion);

        final var projectNames = new ArrayList<>(stdlibProjects.keySet());
        projectNames.sort(String::compareTo);

        final var projectTable = new ProjectTable();
        final var fileTable = new FileTable(projectNames.size());
        final var projectIds = new ArrayList<ProjectId>(projectNames.size());

        for (final var projectName : projectNames) {
            final var projectFiles = stdlibProjects.get(projectName);
            final var projectRoot = projectFiles.projectRoot();
            final var projectId = projectTable.register(ProjectDescriptor.builder()
                    .rootPath(projectRoot)
                    .name(projectName)
                    .version("stdlib-" + stdlibVersion)
                    .sourceRoots(ReadOnlyList.wrap(List.of(projectRoot)))
                    .sourceKind(SourceKind.SDK_INTERFACE)
                    .build());

            projectIds.add(projectId);
            for (final var file : projectFiles.sourceFiles()) {
                registerFile(projectId, projectRoot, file, fileTable);
            }
        }

        final var context = new FrontendPhaseContext(
                projectTable,
                fileTable,
                new BuildStack(ReadOnlyList.wrap(projectIds)),
                stdlibVersion);
        final var diagnostics = DiagnosticSink.empty();

        new PBSFrontendPhaseService().compile(
                context,
                diagnostics,
                LogAggregator.empty(),
                BuildingIssueSink.empty());

        assertEquals(0, diagnostics.errorCount(), () -> diagnosticsSummary(stdlibVersion, diagnostics));
        if (flags.failOnWarnings()) {
            assertEquals(0, diagnostics.warningCount(), () -> diagnosticsSummary(stdlibVersion, diagnostics));
        }
    }

    private Map<String, StdlibProjectFiles> discoverStdlibProjects(
            final int stdlibVersion) throws Exception {
        final var resourcePath = "stdlib/" + stdlibVersion;
        final var classLoader = PbsGateUStdlibCompileTest.class.getClassLoader();
        final var resource = classLoader.getResource(resourcePath);
        assertNotNull(resource, "Stdlib version " + stdlibVersion + " was not found on classpath at " + resourcePath);
        assertEquals("file", resource.getProtocol(),
                "Gate U stdlib discovery only supports file-based resources, got protocol: " + resource.getProtocol());
        return discoverStdlibProjects(Path.of(resource.toURI()), stdlibVersion);
    }

    private Map<String, StdlibProjectFiles> discoverStdlibProjects(
            final Path stdlibRoot,
            final int stdlibVersion) throws IOException {
        final var projectRoots = new LinkedHashMap<String, Path>();
        final var filesByProject = new LinkedHashMap<String, List<Path>>();

        final List<Path> moduleDirs;
        try (final var walk = Files.walk(stdlibRoot)) {
            moduleDirs = walk
                    .filter(Files::isRegularFile)
                    .filter(path -> path.getFileName().toString().equals("mod.barrel"))
                    .map(Path::getParent)
                    .sorted()
                    .toList();
        }
        assertFalse(moduleDirs.isEmpty(), "No stdlib modules (mod.barrel) were found under " + stdlibRoot);

        for (final var moduleDir : moduleDirs) {
            final var relativeModulePath = stdlibRoot.relativize(moduleDir);
            assertTrue(relativeModulePath.getNameCount() >= 2,
                    "Invalid stdlib module path for version " + stdlibVersion + ": " + relativeModulePath);

            final var projectName = relativeModulePath.getName(0).toString();
            final var projectRoot = stdlibRoot.resolve(projectName);
            projectRoots.putIfAbsent(projectName, projectRoot);

            final List<Path> moduleFiles;
            try (final var moduleChildren = Files.list(moduleDir)) {
                moduleFiles = moduleChildren
                        .filter(Files::isRegularFile)
                        .filter(this::isPbsOrBarrelFile)
                        .sorted()
                        .toList();
            }
            assertTrue(moduleFiles.stream().anyMatch(path -> path.getFileName().toString().endsWith(".pbs")),
                    "Stdlib module has no .pbs source file: " + relativeModulePath);
            filesByProject.computeIfAbsent(projectName, ignored -> new ArrayList<>()).addAll(moduleFiles);
        }

        final var result = new LinkedHashMap<String, StdlibProjectFiles>();
        for (final var entry : filesByProject.entrySet()) {
            final var projectName = entry.getKey();
            final var files = entry.getValue().stream().distinct().sorted().toList();
            result.put(projectName, new StdlibProjectFiles(projectRoots.get(projectName), files));
        }
        return result;
    }

    private boolean isPbsOrBarrelFile(final Path file) {
        final var fileName = file.getFileName().toString();
        return fileName.endsWith(".pbs") || fileName.endsWith(".barrel");
    }

    private void registerFile(
            final ProjectId projectId,
            final Path projectRoot,
            final Path file,
            final FileTable fileTable) throws IOException {
        final BasicFileAttributes attributes = Files.readAttributes(file, BasicFileAttributes.class);
        fileTable.register(new SourceHandle(
                projectId,
                projectRoot.relativize(file),
                file,
                attributes.size(),
                attributes.lastModifiedTime().toMillis(),
                SourceProviderFactory.filesystem()));
    }

    private String diagnosticsSummary(
            final int stdlibVersion,
            final DiagnosticSink diagnostics) {
        final var diagnosticsDump = diagnostics.stream()
                .map(Object::toString)
                .collect(Collectors.joining("\n"));
        return "Gate U stdlib compile failed for stdlib=" + stdlibVersion
                + " with errors=" + diagnostics.errorCount()
                + ", warnings=" + diagnostics.warningCount()
                + "\n"
                + diagnosticsDump;
    }

    private record StdlibProjectFiles(
            Path projectRoot,
            List<Path> sourceFiles) {
    }

    private record GateCase(
            String label,
            int stdlibVersion,
            GateFlags flags) {
        @Override
        public String toString() {
            return label;
        }
    }

    private record GateFlags(
            boolean failOnWarnings) {
        private static GateFlags errorsOnly() {
            return new GateFlags(false);
        }
    }
}
