package p.studio.compiler.models;

import org.junit.jupiter.api.Test;
import p.studio.compiler.source.Span;
import p.studio.compiler.source.identifiers.CallableId;
import p.studio.compiler.source.identifiers.FileId;
import p.studio.compiler.source.identifiers.IntrinsicId;
import p.studio.compiler.source.identifiers.ModuleId;
import p.studio.compiler.source.tables.CallableSignatureRef;
import p.studio.compiler.source.tables.IntrinsicReference;
import p.studio.compiler.source.tables.ModuleReference;
import p.studio.utilities.structures.ReadOnlyList;

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

class IRBackendExecutableContractTest {

    @Test
    void callInstructionMustRequireCategorySpecificMetadata() {
        assertThrows(IllegalArgumentException.class, () -> new IRBackendExecutableFunction.Instruction(
                IRBackendExecutableFunction.InstructionKind.CALL_HOST,
                ModuleId.none(),
                "",
                null,
                null,
                Span.none()));

        final var callFunc = new IRBackendExecutableFunction.Instruction(
                IRBackendExecutableFunction.InstructionKind.CALL_FUNC,
                ModuleId.none(),
                "foo",
                new CallableId(7),
                null,
                null,
                Span.none());
        assertEquals(IRBackendExecutableFunction.InstructionKind.CALL_FUNC, callFunc.kind());
    }

    @Test
    void functionContractMustRejectInvalidSlotAndSpanBounds() {
        assertThrows(IllegalArgumentException.class, () -> new IRBackendExecutableFunction(
                new FileId(1),
                "main",
                new CallableId(1),
                10,
                5,
                0,
                0,
                0,
                1,
                ReadOnlyList.empty(),
                Span.none()));

        final var thrown = assertThrows(IllegalArgumentException.class, () -> new IRBackendExecutableFunction(
                new FileId(1),
                "main",
                new CallableId(1),
                0,
                10,
                0,
                0,
                2,
                1,
                ReadOnlyList.empty(),
                Span.none()));
        assertTrue(thrown.getMessage().contains("maxStackSlots"));
    }

    @Test
    void instructionContractMustRejectMixedMetadataKinds() {
        assertThrows(IllegalArgumentException.class, () -> new IRBackendExecutableFunction.Instruction(
                IRBackendExecutableFunction.InstructionKind.CALL_FUNC,
                ModuleId.none(),
                "foo",
                new CallableId(1),
                new IRBackendExecutableFunction.HostCallMetadata("gfx", "draw", 1, 0, 0),
                null,
                Span.none()));

        assertThrows(IllegalArgumentException.class, () -> new IRBackendExecutableFunction.HostCallMetadata(
                "gfx",
                "draw",
                1,
                -1,
                0));
    }

    @Test
    void jumpAndLabelContractMustRequireNames() {
        assertThrows(IllegalArgumentException.class, () -> new IRBackendExecutableFunction.Instruction(
                IRBackendExecutableFunction.InstructionKind.LABEL,
                ModuleId.none(),
                "",
                null,
                null,
                null,
                "",
                "",
                null,
                null,
                Span.none()));

        assertThrows(IllegalArgumentException.class, () -> new IRBackendExecutableFunction.Instruction(
                IRBackendExecutableFunction.InstructionKind.JMP,
                ModuleId.none(),
                "",
                null,
                null,
                null,
                "",
                "",
                null,
                null,
                Span.none()));
    }

    @Test
    void aggregatorMustPreserveExecutableFunctionOrderDeterministically() {
        final var fileA = new IRBackendFile(
                new FileId(1),
                ReadOnlyList.empty(),
                ReadOnlyList.from(new IRBackendExecutableFunction(
                        new FileId(1),
                        "entry",
                        new CallableId(0),
                        0,
                        10,
                        0,
                        0,
                        0,
                        1,
                        ReadOnlyList.from(new IRBackendExecutableFunction.Instruction(
                                IRBackendExecutableFunction.InstructionKind.HALT,
                                "",
                                null,
                                null,
                                Span.none())),
                        Span.none())),
                IRReservedMetadata.empty(),
                ReadOnlyList.from(new CallableSignatureRef("entry", 0, "() -> unit")),
                ReadOnlyList.empty());
        final var fileB = new IRBackendFile(
                new FileId(2),
                ReadOnlyList.empty(),
                ReadOnlyList.from(new IRBackendExecutableFunction(
                        new FileId(2),
                        "aux",
                        new CallableId(0),
                        11,
                        20,
                        0,
                        0,
                        0,
                        1,
                        ReadOnlyList.from(new IRBackendExecutableFunction.Instruction(
                                IRBackendExecutableFunction.InstructionKind.RET,
                                "",
                                null,
                                null,
                                Span.none())),
                        Span.none())),
                IRReservedMetadata.empty(),
                ReadOnlyList.from(new CallableSignatureRef("aux", 0, "() -> unit")),
                ReadOnlyList.empty());

        final var aggregator = IRBackend.aggregator();
        aggregator.merge(fileA);
        aggregator.merge(fileB);
        final var backend = aggregator.emit();

        assertEquals(2, backend.getExecutableFunctions().size());
        assertEquals("main", backend.getEntryPointCallableName());
        assertEquals("entry", backend.getExecutableFunctions().get(0).callableName());
        assertEquals("aux", backend.getExecutableFunctions().get(1).callableName());
        assertEquals(2, backend.getCallableSignatures().size());
    }

    @Test
    void aggregatorMustEmitConfiguredEntrypointCallableName() {
        final var aggregator = IRBackend.aggregator();
        aggregator.entryPointCallableName("frame");

        final var backend = aggregator.emit();
        assertEquals("frame", backend.getEntryPointCallableName());
        assertTrue(backend.getEntryPointModuleId().isNone());
    }

    @Test
    void aggregatorMustEmitConfiguredEntrypointModuleId() {
        final var aggregator = IRBackend.aggregator();
        aggregator.entryPointCallableName("frame");
        aggregator.entryPointModuleId(new ModuleId(3));

        final var backend = aggregator.emit();
        assertEquals("frame", backend.getEntryPointCallableName());
        assertEquals(3, backend.getEntryPointModuleId().getIndex());
    }

    @Test
    void aggregatorMustReindexIntrinsicsToBuildGlobalPool() {
        final var fileA = new IRBackendFile(
                new FileId(1),
                ReadOnlyList.empty(),
                ReadOnlyList.from(new IRBackendExecutableFunction(
                        new FileId(1),
                        "entry",
                        new CallableId(0),
                        0,
                        10,
                        0,
                        0,
                        0,
                        1,
                        ReadOnlyList.from(new IRBackendExecutableFunction.Instruction(
                                IRBackendExecutableFunction.InstructionKind.CALL_INTRINSIC,
                                "",
                                null,
                                new IRBackendExecutableFunction.IntrinsicCallMetadata("core.color.pack", 1, new IntrinsicId(0)),
                                Span.none())),
                        Span.none())),
                IRReservedMetadata.empty(),
                ReadOnlyList.from(new CallableSignatureRef("entry", 0, "() -> unit")),
                ReadOnlyList.from(new IntrinsicReference("core.color.pack", 1)));
        final var fileB = new IRBackendFile(
                new FileId(2),
                ReadOnlyList.empty(),
                ReadOnlyList.from(new IRBackendExecutableFunction(
                        new FileId(2),
                        "aux",
                        new CallableId(0),
                        11,
                        20,
                        0,
                        0,
                        0,
                        1,
                        ReadOnlyList.from(new IRBackendExecutableFunction.Instruction(
                                IRBackendExecutableFunction.InstructionKind.CALL_INTRINSIC,
                                "",
                                null,
                                new IRBackendExecutableFunction.IntrinsicCallMetadata("core.color.pack", 1, new IntrinsicId(0)),
                                Span.none())),
                        Span.none())),
                IRReservedMetadata.empty(),
                ReadOnlyList.from(new CallableSignatureRef("aux", 0, "() -> unit")),
                ReadOnlyList.from(new IntrinsicReference("core.color.pack", 1)));

        final var aggregator = IRBackend.aggregator();
        aggregator.merge(fileA);
        aggregator.merge(fileB);
        final var backend = aggregator.emit();

        final var firstIntrinsicId = backend.getExecutableFunctions().get(0).instructions().getFirst().intrinsicCall().intrinsicId();
        final var secondIntrinsicId = backend.getExecutableFunctions().get(1).instructions().getFirst().intrinsicCall().intrinsicId();
        assertEquals(1, backend.getIntrinsicPool().size());
        assertEquals(firstIntrinsicId, secondIntrinsicId);
        assertEquals("core.color.pack", backend.getIntrinsicPool().getFirst().canonicalName());
    }

    @Test
    void aggregatorMustPreserveExecutableSourceAttribution() {
        final var instructionSpan = new Span(new FileId(7), 21, 34);
        final var functionSpan = new Span(new FileId(7), 10, 40);
        final var file = new IRBackendFile(
                new FileId(7),
                ReadOnlyList.empty(),
                ReadOnlyList.from(new IRBackendExecutableFunction(
                        new FileId(7),
                        "main",
                        new CallableId(0),
                        10,
                        40,
                        0,
                        0,
                        0,
                        1,
                        ReadOnlyList.from(new IRBackendExecutableFunction.Instruction(
                                IRBackendExecutableFunction.InstructionKind.RET,
                                "",
                                null,
                                null,
                                instructionSpan)),
                        functionSpan)),
                IRReservedMetadata.empty(),
                ReadOnlyList.from(new CallableSignatureRef("main", 0, "() -> unit")),
                ReadOnlyList.empty());

        final var aggregator = IRBackend.aggregator();
        aggregator.merge(file);
        final var backend = aggregator.emit();
        final var executable = backend.getExecutableFunctions().getFirst();
        final var emittedInstructionSpan = executable.instructions().getFirst().span();

        assertEquals(new FileId(7), executable.fileId());
        assertEquals(10, executable.sourceStart());
        assertEquals(40, executable.sourceEnd());
        assertEquals(7, emittedInstructionSpan.getFileId().getId());
        assertEquals(21, emittedInstructionSpan.getStart());
        assertEquals(34, emittedInstructionSpan.getEnd());
    }

    @Test
    void aggregatorMustReindexModulesIntoGlobalDensePool() {
        final var fileA = new IRBackendFile(
                new FileId(1),
                ReadOnlyList.empty(),
                ReadOnlyList.from(new IRBackendExecutableFunction(
                        new FileId(1),
                        new ModuleId(0),
                        "entry",
                        new CallableId(0),
                        0,
                        5,
                        0,
                        0,
                        0,
                        1,
                        ReadOnlyList.from(new IRBackendExecutableFunction.Instruction(
                                IRBackendExecutableFunction.InstructionKind.RET,
                                "",
                                null,
                                null,
                                Span.none())),
                        Span.none())),
                IRReservedMetadata.empty(),
                ReadOnlyList.from(new ModuleReference("app", ReadOnlyList.from("mod", "a"))),
                ReadOnlyList.from(new CallableSignatureRef(new ModuleId(0), "entry", 0, "() -> unit")),
                ReadOnlyList.empty());
        final var fileB = new IRBackendFile(
                new FileId(2),
                ReadOnlyList.empty(),
                ReadOnlyList.from(new IRBackendExecutableFunction(
                        new FileId(2),
                        new ModuleId(0),
                        "aux",
                        new CallableId(0),
                        6,
                        10,
                        0,
                        0,
                        0,
                        1,
                        ReadOnlyList.from(new IRBackendExecutableFunction.Instruction(
                                IRBackendExecutableFunction.InstructionKind.RET,
                                "",
                                null,
                                null,
                                Span.none())),
                        Span.none())),
                IRReservedMetadata.empty(),
                ReadOnlyList.from(new ModuleReference("app", ReadOnlyList.from("mod", "b"))),
                ReadOnlyList.from(new CallableSignatureRef(new ModuleId(0), "aux", 0, "() -> unit")),
                ReadOnlyList.empty());

        final var aggregator = IRBackend.aggregator();
        aggregator.merge(fileA);
        aggregator.merge(fileB);
        final var backend = aggregator.emit();

        assertEquals(2, backend.getModulePool().size());
        assertEquals("mod", backend.getModulePool().get(0).pathSegments().get(0));
        assertEquals("a", backend.getModulePool().get(0).pathSegments().get(1));
        assertEquals("b", backend.getModulePool().get(1).pathSegments().get(1));
        assertEquals(0, backend.getExecutableFunctions().get(0).moduleId().getIndex());
        assertEquals(1, backend.getExecutableFunctions().get(1).moduleId().getIndex());
        assertEquals(0, backend.getCallableSignatures().get(0).moduleId().getIndex());
        assertEquals(1, backend.getCallableSignatures().get(1).moduleId().getIndex());
    }
}
