package p.studio.compiler.integration;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import p.studio.compiler.backend.bytecode.BytecodeModule;
import p.studio.utilities.structures.ReadOnlyList;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.file.Files;
import java.nio.file.Path;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

class RuntimeBackedCompatibilityAdapterTest {

    @Test
    void checkMustFallbackToLocalModeWhenRuntimeCommandIsUnavailable() {
        final var adapter = new RuntimeBackedCompatibilityAdapter(
                new LocalRuntimeCompatibilityAdapter(),
                Path.of("./missing-runtime"),
                "");
        final var module = new BytecodeModule(
                0,
                ReadOnlyList.empty(),
                ReadOnlyList.from(new BytecodeModule.FunctionMeta(0, 2, 0, 0, 0, 1)),
                codeRet(),
                null,
                ReadOnlyList.empty(),
                ReadOnlyList.empty());

        final var result = adapter.check(module);
        assertEquals(CompatibilityError.NONE, result.error());
        assertEquals("local-fallback", result.adapterMode());
        assertTrue(!result.reason().isBlank());
        assertTrue(!result.runtimeLine().isBlank());
    }

    @Test
    void checkMustFailInStrictModeWhenRuntimeCommandIsUnavailable() {
        final var adapter = new RuntimeBackedCompatibilityAdapter(
                new LocalRuntimeCompatibilityAdapter(),
                Path.of("."),
                "",
                true);
        final var module = moduleWithSingleRet();

        final var result = adapter.check(module);

        assertEquals(CompatibilityError.RUNTIME_BACKED_REQUIRED, result.error());
        assertEquals("local-fallback", result.adapterMode());
        assertTrue(result.reason().contains("strict gate-i requires runtime-backed adapter"));
    }

    @Test
    void checkMustPassInStrictModeWhenRuntimeCommandIsValid() {
        final var adapter = new RuntimeBackedCompatibilityAdapter(
                new LocalRuntimeCompatibilityAdapter(),
                Path.of("."),
                "echo runtime-check-ok",
                true);
        final var module = moduleWithSingleRet();

        final var result = adapter.check(module);

        assertEquals(CompatibilityError.NONE, result.error());
        assertEquals("runtime-backed", result.adapterMode());
        assertTrue(result.reason().contains("accepted artifact"));
    }

    @Test
    void checkMustBeRepeatableAcrossDeclaredRuntimeLinesInStrictMode(
            @TempDir final Path tempDir) throws IOException {
        final var runtimeLineA = tempDir.resolve("runtime-a");
        final var runtimeLineB = tempDir.resolve("runtime-b");
        Files.createDirectories(runtimeLineA);
        Files.createDirectories(runtimeLineB);
        Files.writeString(runtimeLineA.resolve("VERSION.txt"), "1.0.0-a\n");
        Files.writeString(runtimeLineB.resolve("VERSION.txt"), "1.0.0-b\n");

        final var adapterA = new RuntimeBackedCompatibilityAdapter(
                new LocalRuntimeCompatibilityAdapter(),
                runtimeLineA,
                "test -f {artifact}",
                true);
        final var adapterB = new RuntimeBackedCompatibilityAdapter(
                new LocalRuntimeCompatibilityAdapter(),
                runtimeLineB,
                "test -f {artifact}",
                true);

        final var firstA = adapterA.check(moduleWithSingleRet());
        final var firstB = adapterB.check(moduleWithSingleRet());
        final var secondA = adapterA.check(moduleWithSingleRet());
        final var secondB = adapterB.check(moduleWithSingleRet());

        assertEquals(CompatibilityError.NONE, firstA.error());
        assertEquals(CompatibilityError.NONE, firstB.error());
        assertEquals("runtime-backed", firstA.adapterMode());
        assertEquals("runtime-backed", firstB.adapterMode());
        assertEquals("runtime-v1.0.0-a", firstA.runtimeLine());
        assertEquals("runtime-v1.0.0-b", firstB.runtimeLine());
        assertEquals(firstA.runtimeLine(), secondA.runtimeLine());
        assertEquals(firstB.runtimeLine(), secondB.runtimeLine());
        assertEquals(firstA.error(), secondA.error());
        assertEquals(firstB.error(), secondB.error());
    }

    private BytecodeModule moduleWithSingleRet() {
        return new BytecodeModule(
                0,
                ReadOnlyList.empty(),
                ReadOnlyList.from(new BytecodeModule.FunctionMeta(0, 2, 0, 0, 0, 1)),
                codeRet(),
                null,
                ReadOnlyList.empty(),
                ReadOnlyList.empty());
    }

    private byte[] codeRet() {
        final var out = ByteBuffer.allocate(2).order(ByteOrder.LITTLE_ENDIAN);
        out.putShort((short) 0x51);
        return out.array();
    }
}
