package p.studio.compiler.integration;

import org.junit.jupiter.api.Test;
import p.studio.compiler.messages.BuilderPipelineConfig;
import p.studio.compiler.models.BuilderPipelineContext;
import p.studio.compiler.source.identifiers.FileId;
import p.studio.compiler.utilities.SourceProviderFactory;
import p.studio.compiler.workspaces.BuilderPipelineService;
import p.studio.utilities.logs.LogAggregator;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;

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

class MainProjectPipelineIntegrationTest {

    @Test
    void analyzeShouldNotWriteProgramBytecode() throws IOException {
        final var projectRoot = projectRoot();
        final var outputPath = resetOutput(projectRoot);
        final var logs = bufferedLogs();
        final var context = BuilderPipelineContext.fromConfig(new BuilderPipelineConfig(false, projectRoot.toString()));

        final var snapshot = assertDoesNotThrow(
                () -> BuilderPipelineService.INSTANCE.analyze(context, logs),
                () -> "analyze unexpectedly failed for " + projectRoot);

        assertNotNull(snapshot.resolvedWorkspace(), "analyze must expose resolved workspace");
        assertNotNull(snapshot.fileTable(), "analyze must expose file table");
        assertNotNull(snapshot.irBackend(), "analyze must expose frontend semantic result");
        assertNotNull(snapshot.diagnostics(), "analyze must expose diagnostics collection");
        assertFalse(Files.exists(outputPath), "analyze must not write output: " + outputPath);
    }

    @Test
    void analyzeShouldReturnStructuredDiagnosticsForOverlayDocumentWithoutThrowing() throws IOException {
        final var projectRoot = projectRoot();
        final var documentPath = projectRoot.resolve("src").resolve("main.pbs").toAbsolutePath().normalize();
        final var logs = bufferedLogs();
        final var invalidOverlay = """
                fn frame() -> void {
                  let broken: int = ;
                }
                """;
        final var context = BuilderPipelineContext.fromConfig(new BuilderPipelineConfig(
                false,
                projectRoot.toString(),
                "core-v1",
                SourceProviderFactory.overlayUtf8(Map.of(documentPath, invalidOverlay))));

        final var snapshot = assertDoesNotThrow(
                () -> BuilderPipelineService.INSTANCE.analyze(context, logs),
                () -> "analyze unexpectedly failed for overlay " + documentPath);

        final var documentDiagnostics = snapshot.diagnostics().stream()
                .filter(issue -> issue.getFileId() != null)
                .filter(issue -> issue.getFileId() >= 0 && issue.getFileId() < snapshot.fileTable().size())
                .filter(issue -> snapshot.fileTable().get(new FileId(issue.getFileId())).getCanonPath().toAbsolutePath().normalize().equals(documentPath))
                .toList();

        assertFalse(documentDiagnostics.isEmpty(), "overlay analysis must surface diagnostics for " + documentPath);
        assertTrue(documentDiagnostics.stream().anyMatch(issue -> issue.isError() && issue.getCode() != null && issue.getStart() != null && issue.getEnd() != null));
    }

    @Test
    void compileShouldProduceInMemoryBytecodeWithoutWritingProgramBytecode() throws IOException {
        final var projectRoot = projectRoot();
        final var outputPath = resetOutput(projectRoot);
        final var logs = bufferedLogs();
        final var context = BuilderPipelineContext.fromConfig(new BuilderPipelineConfig(false, projectRoot.toString()));

        final var result = assertDoesNotThrow(
                () -> BuilderPipelineService.INSTANCE.compile(context, logs),
                () -> "compile unexpectedly failed for " + projectRoot);

        assertNotNull(result.analysisSnapshot(), "compile must retain analysis snapshot");
        assertNotNull(result.diagnostics(), "compile must expose diagnostics collection");
        assertNotNull(result.bytecodeModule(), "compile must expose bytecode module");
        assertNotNull(result.bytecodeBytes(), "compile must expose bytecode bytes");
        assertTrue(result.bytecodeBytes().length > 0, "compile must expose non-empty bytecode bytes");
        assertFalse(Files.exists(outputPath), "compile must not write output: " + outputPath);
    }

    @Test
    void buildShouldWriteProgramBytecode() throws IOException {
        final var repoRoot = findRepoRoot(Path.of("").toAbsolutePath().normalize());
        final var projectRoot = repoRoot.resolve("test-projects").resolve("main");
        final var outputPath = resetOutput(projectRoot);
        final var logs = bufferedLogs();
        final var context = BuilderPipelineContext.fromConfig(new BuilderPipelineConfig(false, projectRoot.toString()));

        final var result = assertDoesNotThrow(
                () -> BuilderPipelineService.INSTANCE.build(context, logs),
                () -> "build unexpectedly failed for " + projectRoot);
        assertNotNull(result.diagnostics(), "build must expose diagnostics collection");
        assertNotNull(result.compileResult(), "build must retain compile payload");
        assertNotNull(result.compileResult().analysisSnapshot(), "build must retain analysis payload");
        assertEquals(outputPath, result.bytecodeArtifactPath(), "build must expose written artifact path");
        assertTrue(Files.exists(outputPath), "build did not write output: " + outputPath);
        assertTrue(Files.size(outputPath) > 0, "build wrote empty bytecode file: " + outputPath);
    }

    private Path projectRoot() {
        final var repoRoot = findRepoRoot(Path.of("").toAbsolutePath().normalize());
        return repoRoot.resolve("test-projects").resolve("main");
    }

    private Path resetOutput(final Path projectRoot) throws IOException {
        final var outputPath = projectRoot.resolve("build").resolve("program.pbx");
        Files.createDirectories(outputPath.getParent());
        Files.deleteIfExists(outputPath);
        return outputPath;
    }

    private LogAggregator bufferedLogs() {
        final var logsOut = new StringBuilder();
        return LogAggregator.with(line -> {
            logsOut.append(line);
            if (!line.endsWith("\n")) {
                logsOut.append('\n');
            }
        });
    }

    private Path findRepoRoot(final Path start) {
        var current = start;
        while (current != null) {
            if (Files.exists(current.resolve("settings.gradle.kts"))
                    && Files.exists(current.resolve("test-projects").resolve("main").resolve("prometeu.json"))) {
                return current;
            }
            current = current.getParent();
        }
        fail("unable to locate repository root from " + start);
        return start;
    }
}
